#include "nmWxTimeDependentSkin.h" #include "nmWxPerforationClosing.h" #include "nmDialogPreviewGuard.h" #include "nmWxPressFlowChartWidget.h" #include "nmWxChartWidget.h" #include "nmPebiResultSnapshot.h" #include "iUnitGroup.h" #include "iUnitHelper.h" #include "ZxDataEditorDelegate.h" #include "iBase_global.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { const double skinPressureUnitFactor = 1e-3 / (86400.0 * 2.0 * 3.14159265358979323846 * 9.869233e-16 * 1e6); bool validSkinPressureCurve(const QVector >& curve) { // 插值只接受同一时间基准下有限、严格递增的压力序列。 if(curve.size() < 2 || curve[0].size() < 2 || curve[0].size() != curve[1].size()) return false; for(int i = 0; i < curve[0].size(); ++i) { if(!qIsFinite(curve[0][i]) || !qIsFinite(curve[1][i]) || (i > 0 && curve[0][i] <= curve[0][i - 1])) return false; } return true; } bool interpolateSkinPressure(const QVector >& curve, double time, double& pressure, bool extrapolate = false) { // 压力取样按步骤末端线性插值、沿端点斜率外推;PVT 查值默认仍限制在表内。 const QVector& times = curve[0]; if(!extrapolate && (time < times.first() || time > times.last())) return false; const int index = static_cast(std::lower_bound(times.constBegin(), times.constEnd(), time) - times.constBegin()); const int right = qBound(1, index, times.size() - 1); const double fraction = (time - times[right - 1]) / (times[right] - times[right - 1]); pressure = curve[1][right - 1] + fraction * (curve[1][right] - curve[1][right - 1]); return qIsFinite(pressure); } // 只保存本次估算用的值数据;液体参考物性读取所属分析,曲线和制度沿用模型快照。 struct SkinComputeData { QVector rates; QVector > pressureMobility; double pressureFactor; }; bool skinPvtCurve(const QVector& pressure, const QVector& values, QVector >& curve) { // PVT 表可按压力升序或降序保存,统一方向后复用压力曲线的校验和插值。 curve.resize(2); curve[0] = pressure; curve[1] = values; if(pressure.size() > 1 && pressure.first() > pressure.last()) { std::reverse(curve[0].begin(), curve[0].end()); std::reverse(curve[1].begin(), curve[1].end()); } if(!validSkinPressureCurve(curve)) return false; for(int i = 0; i < curve[0].size(); ++i) { if(curve[0][i] <= 0.0 || curve[1][i] <= 0.0) return false; } return true; } QString prepareSkinCompute(const nmPebiResultWellSnapshot& well, const nmPebiResultReservoirParameters& reservoir, const nmPebiResultPvtParameters& pvt, int modelType, const QMap& referencePvt, SkinComputeData& data) { data = SkinComputeData(); // mD、m、mPa.s、m³/d、MPa 的单位换算;普通压差用参考物性,气拟压力模式保留积分。 data.pressureFactor = reservoir.m_dPermeability * reservoir.m_dThickness / skinPressureUnitFactor; if(!qIsFinite(reservoir.m_dPermeability) || reservoir.m_dPermeability <= 0.0 || !qIsFinite(reservoir.m_dThickness) || reservoir.m_dThickness <= 0.0 || !qIsFinite(data.pressureFactor) || data.pressureFactor <= 0.0 || !qIsFinite(well.m_dSkin)) return nmWxTimeDependentSkin::tr("The model requires valid permeability, thickness and skin."); const QVector* volume = nullptr; const QVector* viscosity = nullptr; double constantVolume = 0.0, constantViscosity = 0.0; NM_PHASE_TYPE phase = PHASE_UNKNOWN; switch(modelType) { case SMT_Oil_ConstPvt: case SMT_Oil_VariablePvt: phase = PHASE_Oil; constantVolume = referencePvt.value("Bo"); constantViscosity = referencePvt.value("Miuo"); break; case SMT_Water_ConstPvt: case SMT_Water_VariablePvt: phase = PHASE_Water; constantVolume = referencePvt.value("Bw"); constantViscosity = referencePvt.value("Miuw"); break; case SMT_Gas_VariablePvt: // 气变化 PVT 按不使用拟压力的方式估算表皮,与油、水共用参考单值换算。 phase = PHASE_Gas; constantVolume = referencePvt.value("Bg"); constantViscosity = referencePvt.value("Miug"); break; case SMT_Gas_PseudoPressure: // 气拟压力模式的结果压力为 MPa;Bg 已包含气体标准条件换算。 phase = PHASE_Gas; volume = &pvt.m_vecBg; viscosity = &pvt.m_vecMiug; break; case SMT_Oil_Water_TwoPhase: break; default: return nmWxTimeDependentSkin::tr("Skin estimation supports single-phase oil, gas, water and oil-water models only."); } if(modelType == SMT_Oil_Water_TwoPhase) { // 按模型读取已冻结的油水制度,只检查数组长度以保证逐段取值有效。 if(well.m_vecOilRates.size() != well.m_vecFlowDurations.size() || well.m_vecWaterRates.size() != well.m_vecFlowDurations.size()) return nmWxTimeDependentSkin::tr("The model rate schedule or pressure conversion factor is invalid."); // 未启用 Perrine:使用模型渗透率和油相参考物性,水量通过 Bw/Bo 换算。 const double bo = referencePvt.value("Bo"), bw = referencePvt.value("Bw"); const double muo = referencePvt.value("Miuo"); if(!qIsFinite(bo) || bo <= 0.0 || !qIsFinite(bw) || bw <= 0.0 || !qIsFinite(muo) || muo <= 0.0) return nmWxTimeDependentSkin::tr("Set positive reference volume factors and viscosities in the analysis PVT settings."); data.pressureFactor /= bo * muo; for(int i = 0; i < well.m_vecFlowDurations.size(); ++i) { const double qo = well.m_vecOilRates[i]; const double qw = well.m_vecWaterRates[i]; // 两相反向流动不适用当前的等效生产流量计算。 if(!qIsFinite(qo) || !qIsFinite(qw) || (qo < 0.0 && qw > 0.0) || (qo > 0.0 && qw < 0.0)) return nmWxTimeDependentSkin::tr("Oil-water estimation requires finite phase rates with the same flow direction."); data.rates.append(qo + qw * bw / bo); } } else { // 求解输入已将未使用的相补零,这里直接取模型对应相的实际计算流量。 data.rates = phase == PHASE_Oil ? well.m_vecOilRates : (phase == PHASE_Water ? well.m_vecWaterRates : well.m_vecGasRates); if(volume) { QVector > volumeCurve, viscosityCurve; if(!skinPvtCurve(pvt.m_vecPressure, *volume, volumeCurve) || !skinPvtCurve(pvt.m_vecPressure, *viscosity, viscosityCurve)) return nmWxTimeDependentSkin::tr("Variable-PVT skin estimation requires finite, positive B and viscosity values on a monotonic pressure table."); // 底层 calPS 依赖全局模式且缺少冻结的配套尺度,这里保留同一快照的常规气体积分。 data.pressureMobility = volumeCurve; for(int i = 0; i < volumeCurve[0].size(); ++i) { const double inverse = 1.0 / (volumeCurve[1][i] * viscosityCurve[1][i]); if(!qIsFinite(inverse) || inverse <= 0.0) return nmWxTimeDependentSkin::tr("Variable-PVT skin estimation requires finite, positive B and viscosity values on a monotonic pressure table."); data.pressureMobility[1][i] = inverse; } } else { // 普通压差模式使用本次读取的参考 B、黏度,不按 Pi 重新查表。 if(!qIsFinite(constantVolume) || constantVolume <= 0.0 || !qIsFinite(constantViscosity) || constantViscosity <= 0.0) return nmWxTimeDependentSkin::tr("Set positive reference volume factors and viscosities in the analysis PVT settings."); data.pressureFactor /= constantVolume * constantViscosity; } } if(!qIsFinite(data.pressureFactor) || data.pressureFactor <= 0.0 || data.rates.isEmpty() || data.rates.size() != well.m_vecFlowDurations.size()) return nmWxTimeDependentSkin::tr("The model rate schedule or pressure conversion factor is invalid."); for(int i = 0; i < data.rates.size(); ++i) { if(!qIsFinite(data.rates[i]) || !qIsFinite(well.m_vecFlowDurations[i]) || well.m_vecFlowDurations[i] <= 0.0) return nmWxTimeDependentSkin::tr("The model rate schedule or pressure conversion factor is invalid."); } return QString(); } bool skinPressureDifference(const SkinComputeData& data, double measuredPressure, double modelPressure, double& difference) { difference = modelPressure - measuredPressure; if(data.pressureMobility.isEmpty()) return qIsFinite(difference); // 直接对冻结 PVT 的 1/(Bμ) 积分,等价于配套系数消去归一化常数后的拟压力差。 // 分段线性插值积分密度;端点也参与积分,避免仅对累计积分值插值的区间内误差。 const double lower = qMin(measuredPressure, modelPressure); const double upper = qMax(measuredPressure, modelPressure); double lowerValue, upperValue; if(!interpolateSkinPressure(data.pressureMobility, lower, lowerValue) || !interpolateSkinPressure(data.pressureMobility, upper, upperValue)) return false; double integral = 0.0, left = lower, leftValue = lowerValue; const QVector& pressure = data.pressureMobility[0]; int index = static_cast(std::upper_bound(pressure.constBegin(), pressure.constEnd(), lower) - pressure.constBegin()); for(; index < pressure.size() && pressure[index] < upper; ++index) { const double value = data.pressureMobility[1][index]; integral += (pressure[index] - left) * (leftValue + value) * 0.5; left = pressure[index]; leftValue = value; } integral += (upper - left) * (leftValue + upperValue) * 0.5; difference = modelPressure >= measuredPressure ? integral : -integral; return qIsFinite(difference); } int estimateSkinSegments(const nmPebiResultWellSnapshot& well, const SkinComputeData& data, const QVector >& measured, QVector& segments) { const QVector >& model = well.m_oCurves.m_vecResultPressure; const QVector& rates = data.rates; if(!validSkinPressureCurve(model) || !validSkinPressureCurve(measured) || rates.size() != well.m_vecFlowDurations.size() || rates.isEmpty()) return 0; QVector ends; double end = 0.0; for(int i = 0; i < rates.size(); ++i) { const double duration = well.m_vecFlowDurations[i]; if(!qIsFinite(duration) || duration <= 0.0 || !qIsFinite(rates[i])) return 0; end += duration; if(!qIsFinite(end)) return 0; ends.append(end); } int computedCount = 0; QVector unmatchedSegments; for(int i = 0; i < segments.size(); ++i) { const double start = segments[i].segmentStart.getValue().toDouble(); const double finish = segments[i].segmentEnd.getValue().toDouble(); if(!qIsFinite(start) || !qIsFinite(finish) || finish <= start) { continue; } // 按 Saphir 取起点落在当前表皮段内的最后一个非零流量段,并在该流量段末端取样。 double sampleTime = 0.0; double rate = 0.0; int rateIndex = -1; for(int j = 0; j < rates.size(); ++j) { const double rateStart = j == 0 ? 0.0 : ends[j - 1]; if(rateStart > finish - 1e-5) break; if(rateStart >= start - 1e-5 && rates[j] != 0.0) { sampleTime = ends[j]; rate = rates[j]; rateIndex = j; } } // 没有符合条件的非零流量段时删除该分界;先完成原始分段取样,避免合并改变本次计算。 if(rateIndex < 0) { unmatchedSegments.append(i); continue; } double modelPressure = 0.0, measuredPressure = 0.0; if(!interpolateSkinPressure(model, sampleTime, modelPressure, true) || !interpolateSkinPressure(measured, sampleTime, measuredPressure, true)) { continue; } double pressureDifference = 0.0; if(!skinPressureDifference(data, measuredPressure, modelPressure, pressureDifference)) { continue; } // 修正始终相对目标井的冻结表皮,重复点击或换井不会累加、串用基准值。 const double skin = well.m_dSkin + data.pressureFactor * pressureDifference / rate; if(!qIsFinite(skin)) { continue; } segments[i].skinValue.setValue(qMax(-5.0, skin)); // 与 Compute 的单点校正一致,只估计总表皮,不从一个压力差同时反演 dS/dQ。 segments[i].dSdQ.setValue(0.0); ++computedCount; } // 从后往前移除无匹配段,前段延伸至被删段末端,保留其已算出的表皮值。 for(int i = unmatchedSegments.size() - 1; i >= 0; --i) { const int index = unmatchedSegments[i]; if(index > 0) segments[index - 1].segmentEnd = segments[index].segmentEnd; segments.remove(index); } return computedCount; } } // 静态成员变量定义 nmWxTimeDependentSkin* nmWxTimeDependentSkin::s_instance = nullptr; void nmWxTimeDependentSkin::showForCurrentWell(nmDataWellBase* pSpecificWellData, QWidget* parent) { // 绑定实际井对象;同名井或不同分析不能共用旧窗口的数据和图表状态。 nmDataWellBase* pWellData = pSpecificWellData; if (!pWellData) { nmDataAnalyzeManager* pManager = nmDataAnalyzeManager::getCurrentInstance(); if (!pManager) return; pWellData = pManager->getCurWellData(); } if (!pWellData) return; if (s_instance) { // 只激活仍在编辑的同一窗口,关闭后重新构造默认的压力/流量界面。 if (s_instance->isVisible() && s_instance->m_currentWell == pWellData && s_instance->parentWidget() == parent) { s_instance->show(); s_instance->raise(); s_instance->activateWindow(); return; } delete s_instance; } // 仅入口创建的堆对象关闭即释放,井编辑器中的栈对象仍由调用方管理。 s_instance = new nmWxTimeDependentSkin(pWellData, parent); s_instance->setAttribute(Qt::WA_DeleteOnClose, true); s_instance->show(); s_instance->raise(); s_instance->activateWindow(); } nmWxTimeDependentSkin::nmWxTimeDependentSkin(nmDataWellBase* pWellData, QWidget *parent) : iDlgBase(parent) , m_pDataManager(nmDataAnalyzeManager::getCurrentInstance()) , m_pChartWidget(nullptr) , m_pOriginalPressureChart(nullptr) , m_pOriginalFlowRateChart(nullptr) , m_isAddMode(false) , m_isRemoveMode(false) , m_currentPerforationIndex(0) , m_pPressureChart(nullptr) , m_pFlowRateChart(nullptr) , m_timeDisplayUnit("hr") , m_dSdQDisplayUnit("1/B/D") , m_pTimeUnitCombo(nullptr) , m_pDSdQUnitCombo(nullptr) , m_rateDependentState(false) { // 根据传入的井数据决定使用哪个数据源 if (pWellData != nullptr) { // 使用传入的特定井数据, 来自nmWxEditWellPlot m_currentWell = pWellData; } else { // 使用当前井数据, 来自nmWxNumericalDesign m_currentWell = nmDataAnalyzeManager::getCurrentInstance()->getCurWellData(); } // 分析关闭或井对象被替换时立即关闭窗口,避免继续访问上一分析的数据。 if (m_currentWell) { connect(m_currentWell, SIGNAL(destroyed(QObject*)), this, SLOT(onWellDestroyed())); } if(m_currentWell != nullptr) { // 获取压力和流量数据 pressurePoints = m_currentWell->getPressurePoints(); QVector rawFlowData = m_currentWell->getFlowPoints(); flowPoints = processFlowData(rawFlowData); // 默认选择第一个射孔段 m_currentPerforationIndex = 0; m_perforationData = m_currentWell->getPerforationCopy(m_currentPerforationIndex); } if(m_currentWell != nullptr) { // 获取压力和流量数据 pressurePoints = m_currentWell->getPressurePoints(); QVector rawFlowData = m_currentWell->getFlowPoints(); flowPoints = processFlowData(rawFlowData); // 默认选择第一个射孔段 m_currentPerforationIndex = 0; m_perforationData = m_currentWell->getPerforationCopy(m_currentPerforationIndex); } // 如果副本为空,初始化默认数据 if(m_perforationData.getFlowSegments().isEmpty()) { initializeDefaultFlowSegment(); } // 确保副本有默认的表皮系数数据 if(m_perforationData.getSkin0().getValue().toString().isEmpty()) { m_perforationData.getSkin0().setValue("0.0"); } if(m_perforationData.getdSkindq().getValue().toString().isEmpty()) { m_perforationData.getdSkindq().setValue("0.0"); } // 确保线条位置有默认值 double x1, y1, x2, y2; m_perforationData.getLinePositions(x1, y1, x2, y2); if(x1 == -999.0 || y1 == -999.0 || x2 == -999.0 || y2 == -999.0) { m_perforationData.setLinePositions(100.0, 0.0, 200.0, 0.0); } setWindowTitle(tr("Time Dependent Skin")); new nmDialogPreviewGuard(this); setMinimumSize(1150, 800); // 初始化UI initUI(); // 初始化射孔段下拉框数据 if(m_currentWell != nullptr) { // 获取射孔段个数并添加到下拉框 int perforationCount = m_currentWell->getPerforationCount(); for(int i = 0; i < perforationCount; ++i) { nmDataPerforation* perforation = m_currentWell->getPerforation(i); if(perforation) { QString name = perforation->getName().getValue().toString(); m_pComboBox->addItem(name); } } // 设置当前选中项为当前射孔段索引 if(m_currentPerforationIndex < perforationCount) { m_pComboBox->setCurrentIndex(m_currentPerforationIndex); } } // 更新图表 updateCharts(); // 初始化表格数据 updateTableFromFlowSegments(); // 设置信号和槽 connect(m_pAddBtn, SIGNAL(clicked()), this, SLOT(onAddClicked())); connect(m_pRemoveBtn, SIGNAL(clicked()), this, SLOT(onRemoveClicked())); connect(m_pSplitStepsBtn, SIGNAL(clicked()), this, SLOT(onSplitStepsClicked())); connect(m_pComputeBtn, SIGNAL(clicked()), this, SLOT(onComputeClicked())); connect(m_pPerforationClosingBtn, SIGNAL(clicked()), this, SLOT(onPerforationClosingBtnClicked())); connect(m_pSkinVsRateBtn, SIGNAL(clicked()), this, SLOT(onSkinVsRateClicked())); connect(m_pOkBtn, SIGNAL(clicked()), this, SLOT(onAccept())); connect(m_pCancelBtn, SIGNAL(clicked()), this, SLOT(onReject())); connect(m_pDataTable, SIGNAL(cellChanged(int, int)), this, SLOT(onTableDataChanged(int, int))); connect(m_pDataTable, SIGNAL(cellClicked(int, int)), this, SLOT(onTableRowClicked(int, int))); // 右侧按钮信号连接 connect(m_pRightAddBtn, SIGNAL(clicked()), this, SLOT(onRightAddClicked())); connect(m_pRightInsertBtn, SIGNAL(clicked()), this, SLOT(onRightInsertClicked())); connect(m_pRightDeleteBtn, SIGNAL(clicked()), this, SLOT(onRightDeleteClicked())); // 流动段操作 connect(m_pPressureChart, SIGNAL(flowSegmentClicked(int)), this, SLOT(onFlowSegmentSelected(int))); connect(m_pFlowRateChart, SIGNAL(flowSegmentClicked(int)), this, SLOT(onFlowSegmentSelected(int))); connect(m_pPressureChart, SIGNAL(chartPointClicked(double)), this, SLOT(onChartPointClicked(double))); connect(m_pFlowRateChart, SIGNAL(chartPointClicked(double)), this, SLOT(onChartPointClicked(double))); // 拖动相关的信号连接 connect(m_pPressureChart, SIGNAL(boundaryMoved(int, double)), this, SLOT(onBoundaryMoved(int, double))); connect(m_pFlowRateChart, SIGNAL(boundaryMoved(int, double)), this, SLOT(onBoundaryMoved(int, double))); connect(m_pPressureChart, SIGNAL(segmentsMerged(int)), this, SLOT(onSegmentsMerged(int))); connect(m_pFlowRateChart, SIGNAL(segmentsMerged(int)), this, SLOT(onSegmentsMerged(int))); connect(m_pSnapToRateChangesCheckBox, SIGNAL(toggled(bool)), this, SLOT(onSnapToRateChangesToggled(bool))); connect(m_pPressureChart, SIGNAL(boundaryMovedFinal(int, double)), this, SLOT(onBoundaryMovedFinal(int, double))); connect(m_pFlowRateChart, SIGNAL(boundaryMovedFinal(int, double)), this, SLOT(onBoundaryMovedFinal(int, double))); connect(m_pAddRateDependentCheckBox, SIGNAL(toggled(bool)), this, SLOT(onAddRateDependentToggled(bool))); connect(m_pShowDatesCheckBox, SIGNAL(toggled(bool)), this, SLOT(onShowDatesToggled(bool))); connect(m_pComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(onPerforationChanged(int))); // 新结果发布或失效后同步按钮状态;点击时仍重新检查,避免使用旧分析结果。 nmDataAnalyzeManager* manager = m_pDataManager.data(); if(manager) connect(manager, SIGNAL(sigPebiResultSnapshotChanged()), this, SLOT(updateComputeState())); updateComputeState(); } nmWxTimeDependentSkin::~nmWxTimeDependentSkin() { // 如果静态实例指针指向当前对象,清空静态变量 if (s_instance == this) { s_instance = nullptr; } } void nmWxTimeDependentSkin::initUI() { // 创建主布局 QVBoxLayout* mainLayout = new QVBoxLayout(this); // 创建工具栏容器 QWidget* toolbarContainer = new QWidget(this); toolbarContainer->setMinimumHeight(70); toolbarContainer->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); m_pToolbarLayout = new QHBoxLayout(toolbarContainer); // 保存布局指针 // 高度由图标、文字和字体共同决定,避免外层边距裁掉按钮下方文字。 m_pToolbarLayout->setContentsMargins(0, 0, 0, 0); // 创建按钮并设置统一样式 auto createButton = [&](QPushButton*& button, const QString & iconName, const QString & text) { QWidget* buttonContainer = new QWidget(this); QVBoxLayout* containerLayout = new QVBoxLayout(buttonContainer); containerLayout->setSpacing(0); containerLayout->setContentsMargins(2, 2, 2, 2); // 减小边距 // 图标按钮 button = new QPushButton(buttonContainer); // 统一使用框架的图标查找和缓存,保留原按钮大小和位置。 button->setIcon(zxLoadIcon(iconName)); button->setIconSize(QSize(36, 36)); button->setFixedSize(40, 40); // 文字标签 QLabel* textLabel = new QLabel(text, buttonContainer); textLabel->setAlignment(Qt::AlignCenter); // 将按钮和文字添加到容器 containerLayout->addWidget(button, 0, Qt::AlignHCenter); containerLayout->addWidget(textLabel); // 保留常规尺寸,大字体时允许布局按文字所需空间扩展。 buttonContainer->setMinimumWidth(100); buttonContainer->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); m_pToolbarLayout->addWidget(buttonContainer); }; // 创建所有按钮 createButton(m_pSkinVsRateBtn, "nmTimeDepSkin1", tr("Skin vs Rate")); createButton(m_pAddBtn, "nmTimeDepSkin2", tr("Add")); createButton(m_pRemoveBtn, "nmTimeDepSkin3", tr("Remove")); createButton(m_pSplitStepsBtn, "nmTimeDepSkin4", tr("Split Steps")); createButton(m_pComputeBtn, "nmTimeDepSkin5", tr("Compute")); createButton(m_pPerforationClosingBtn, "nmTimeDepSkin6", tr("Perforation Closing")); m_pToolbarLayout->addStretch(); // 创建复选框区域 QWidget* checkboxContainer = new QWidget(this); QHBoxLayout* checkboxLayout = new QHBoxLayout(checkboxContainer); checkboxLayout->setSpacing(15); // 设置复选框之间的间距 checkboxLayout->setContentsMargins(10, 5, 10, 5); // 创建三个复选框 m_pSnapToRateChangesCheckBox = new QCheckBox(tr("Snap to rate changes")); m_pShowDatesCheckBox = new QCheckBox(tr("Show dates")); m_pAddRateDependentCheckBox = new QCheckBox(tr("Add rate dependent")); // 将复选框水平添加到布局 checkboxLayout->addWidget(m_pSnapToRateChangesCheckBox); checkboxLayout->addWidget(m_pShowDatesCheckBox); checkboxLayout->addWidget(m_pAddRateDependentCheckBox); // 大字体或翻译文字较长时,按实际内容扩展复选框区域。 checkboxContainer->setMinimumWidth(450); checkboxContainer->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); // 将复选框容器添加到工具栏布局 m_pToolbarLayout->addWidget(checkboxContainer); // 将工具栏容器添加到主布局 mainLayout->addWidget(toolbarContainer); // 创建分割器,用于调整三个区域的大小 QSplitter* splitter = new QSplitter(Qt::Horizontal, this); splitter->setChildrenCollapsible(false); splitter->setHandleWidth(1); // 创建左侧区域(图表+下拉框) QWidget* leftWidget = new QWidget(splitter); QVBoxLayout* leftLayout = new QVBoxLayout(leftWidget); // 将左侧布局留白让给图表标题,绘图区用相同宽度补偿,保持位置和大小。 int chartLeftExtension, topMargin, rightMargin, bottomMargin; leftLayout->getContentsMargins(&chartLeftExtension, &topMargin, &rightMargin, &bottomMargin); leftLayout->setContentsMargins(0, topMargin, rightMargin, bottomMargin); // 创建图表区域容器 QWidget* chartWidget = new QWidget(); m_pChartLayout = new QVBoxLayout(chartWidget); m_pChartLayout->setSpacing(0); // 移除布局间距,让分割器来控制间距 m_pChartLayout->setContentsMargins(0, 0, 0, 0); // 移除边距 // 创建垂直分割器用于调整两个图表的比例 m_pChartSplitter = new QSplitter(Qt::Vertical, chartWidget); m_pChartSplitter->setChildrenCollapsible(false); // 防止子控件被完全折叠 m_pChartSplitter->setHandleWidth(3); // 设置分割线宽度 // 创建压力图表 m_pPressureChart = new nmWxPressFlowChartWidget(m_pChartSplitter); m_pPressureChart->setContentsMargins(chartLeftExtension, 0, 0, 0); m_pPressureChart->setMinimumWidth(300 + chartLeftExtension); m_pPressureChart->setCurrentWellData(m_currentWell); m_pPressureChart->setAxisLabels(tr(""), tr("Pressure")); m_pPressureChart->setAxisUnits(tr("hr"), tr("MPa")); m_pPressureChart->setLineColor(QColor(50, 200, 50)); // 绿色 m_pPressureChart->setChartType(CHART_TYPE_LINE); m_pPressureChart->setMinimumHeight(150); // 设置最小高度防止过度压缩 m_pPressureChart->setShowXAxisLabels(false); // 压力图不显示X轴标签 // 创建流量图表 m_pFlowRateChart = new nmWxPressFlowChartWidget(m_pChartSplitter); m_pFlowRateChart->setContentsMargins(chartLeftExtension, 0, 0, 0); m_pFlowRateChart->setMinimumWidth(300 + chartLeftExtension); m_pFlowRateChart->setCurrentWellData(m_currentWell); m_pFlowRateChart->setAxisLabels(tr("Time"), tr("Flow")); m_pFlowRateChart->setAxisUnits(tr("hr"), tr("m^3/D")); m_pFlowRateChart->setLineColor(QColor(255, 0, 0)); // 红色 m_pFlowRateChart->setChartType(CHART_TYPE_STEP); m_pFlowRateChart->setMinimumHeight(150); // 设置最小高度防止过度压缩 // 保存原始图表的引用 m_pOriginalPressureChart = m_pPressureChart; m_pOriginalFlowRateChart = m_pFlowRateChart; // 将图表添加到分割器 m_pChartSplitter->addWidget(m_pPressureChart); m_pChartSplitter->addWidget(m_pFlowRateChart); // 设置初始比例 (1:1) m_pChartSplitter->setStretchFactor(0, 1); m_pChartSplitter->setStretchFactor(1, 1); // 将分割器添加到布局中 m_pChartLayout->addWidget(m_pChartSplitter, 1); // 创建图表下方的下拉框 // 使用普通下拉框,按现有固定宽度显示完整的射孔名称。 m_pComboBox = new QComboBox(); m_pComboBox->setFixedWidth(240); // 加宽下拉框,便于查看射孔段名称 // 将图表和下拉框添加到左侧布局 leftLayout->addWidget(chartWidget, 1); // 图表占据大部分空间 // 下拉框仍使用原来的左边距,只有图表标题向左扩展。 QHBoxLayout* comboLayout = new QHBoxLayout(); comboLayout->setContentsMargins(0, 0, 0, 0); comboLayout->addSpacing(chartLeftExtension); comboLayout->addWidget(m_pComboBox); comboLayout->addStretch(); leftLayout->addLayout(comboLayout); // 创建中间区域(表格+确定取消按钮) QWidget* middleWidget = new QWidget(); QVBoxLayout* middleLayout = new QVBoxLayout(middleWidget); // 创建数据表格 m_pDataTable = new QTableWidget(0, 3); // 设置列标题 QStringList headers; headers << tr("") << tr("Start time") << tr("Skin"); m_pDataTable->setHorizontalHeaderLabels(headers); // 沿用敏感性分析的行号、隔行底色和表头样式,单位行不计入分段编号。 m_pDataTable->verticalHeader()->setVisible(true); m_pDataTable->verticalHeader()->setMovable(false); m_pDataTable->verticalHeader()->setDefaultAlignment(Qt::AlignCenter); m_pDataTable->verticalHeader()->setMinimumWidth(18); m_pDataTable->setAlternatingRowColors(true); m_pDataTable->setShowGrid(true); m_pDataTable->setEditTriggers(QAbstractItemView::DoubleClicked); m_pDataTable->setStyleSheet( "QTableWidget {" " background-color: palette(base);" " alternate-background-color: rgb(232, 232, 232);" " gridline-color: palette(midlight);" " border: 1px solid palette(mid);" "}" "QHeaderView {" " background-color: palette(button);" " color: palette(button-text);" "}" "QHeaderView::section {" " background-color: palette(button);" " color: palette(button-text);" " border: none;" " border-right: 1px solid palette(mid);" " border-bottom: 1px solid palette(mid);" "}" "QTableCornerButton::section {" " background-color: palette(base);" " border: none;" " border-right: 1px solid palette(mid);" " border-bottom: 1px solid palette(mid);" "}"); m_pDataTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_pDataTable->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); QHeaderView* header = m_pDataTable->horizontalHeader(); header->setHighlightSections(false); header->setResizeMode(0, QHeaderView::Fixed); // 第一列(箭头列)固定宽度 header->setResizeMode(1, QHeaderView::Stretch); // 第二列(时间列)拉伸 header->setResizeMode(2, QHeaderView::Stretch); // 第三列(Skin列)拉伸 // 表头下的“单位”行 m_pDataTable->insertRow(0); // 初次创建和切换表格结构时共用同一套框架单位控件。 createTimeUnitCombo(); m_pDataTable->setItemDelegate(new ZxDataEditorDelegate(m_pDataTable)); // 第三列:占位(Skin 无单位) QTableWidgetItem* empty = new QTableWidgetItem(""); empty->setFlags(Qt::ItemIsEnabled); m_pDataTable->setItem(0, 2, empty); // 行高 m_pDataTable->setRowHeight(0, 24); // 设置第一列的固定宽度 m_pDataTable->setColumnWidth(0, 20); // 表格设置 m_pDataTable->setSelectionMode(QAbstractItemView::NoSelection); // 禁用默认选中 m_pDataTable->setFocusPolicy(Qt::NoFocus); // 禁用焦点 // 创建表格下方的确定取消按钮 QHBoxLayout* tableBottomLayout = new QHBoxLayout(); tableBottomLayout->addStretch(); m_pOkBtn = new QPushButton(tr("OK")); m_pCancelBtn = new QPushButton(tr("Cancel")); m_pOkBtn->setFixedSize(100, 30); m_pCancelBtn->setFixedSize(100, 30); tableBottomLayout->addWidget(m_pOkBtn); tableBottomLayout->addWidget(m_pCancelBtn); // 将表格和按钮添加到中间布局 middleLayout->addWidget(m_pDataTable, 1); // 表格占据大部分空间 middleLayout->addLayout(tableBottomLayout); // 确定取消按钮 // 创建右侧按钮区域 QWidget* rightButtonWidget = new QWidget(); QVBoxLayout* rightButtonLayout = new QVBoxLayout(rightButtonWidget); // 创建三个垂直排布的按钮 m_pRightAddBtn = new QPushButton(tr("Add")); m_pRightInsertBtn = new QPushButton(tr("Insert")); m_pRightDeleteBtn = new QPushButton(tr("Delete")); // 设置按钮的固定大小 m_pRightAddBtn->setFixedSize(100, 30); m_pRightInsertBtn->setFixedSize(100, 30); m_pRightDeleteBtn->setFixedSize(100, 30); // 添加按钮到垂直布局 rightButtonLayout->addWidget(m_pRightAddBtn); rightButtonLayout->addWidget(m_pRightInsertBtn); rightButtonLayout->addWidget(m_pRightDeleteBtn); rightButtonLayout->addStretch(); // 添加弹性空间,将按钮推到顶部 // 将三个区域直接添加到分割器 splitter->addWidget(leftWidget); // 左侧区域(图表+下拉框) splitter->addWidget(middleWidget); // 中间区域(表格+确定取消按钮) splitter->addWidget(rightButtonWidget); // 右侧区域(Add/Insert/Delete按钮) // 设置比例 splitter->setStretchFactor(0, 6); // 左侧区域占7份 splitter->setStretchFactor(1, 3); // 中间区域占2份 splitter->setStretchFactor(2, 1); // 右侧区域占1份 mainLayout->addWidget(splitter); // 设置主布局 setLayout(mainLayout); // 工具栏按钮默认状态 m_pSkinVsRateBtn->setEnabled(false); m_pAddBtn->setEnabled(true); m_pRemoveBtn->setEnabled(true); m_pSplitStepsBtn->setEnabled(true); m_pComputeBtn->setEnabled(false); m_pPerforationClosingBtn->setEnabled(true); // 复选框默认状态 m_pSnapToRateChangesCheckBox->setChecked(true); // 从数据中读取"对齐到流量段"的状态 bool snapToRateEnabled = m_perforationData.isSnapToRateChangesEnabled(); m_pSnapToRateChangesCheckBox->setChecked(snapToRateEnabled); // 从数据中读取"显示日期"的状态 bool showDatesEnabled = m_perforationData.isShowDatesEnabled(); m_pShowDatesCheckBox->setChecked(showDatesEnabled); // 从数据中读取"添加流量相关"的状态 bool rateDependentEnabled = m_perforationData.isRateDependentEnabled(); m_pAddRateDependentCheckBox->setChecked(rateDependentEnabled); m_rateDependentState = rateDependentEnabled; // 保存初始状态 // 右侧按钮默认状态 m_pRightAddBtn->setEnabled(true); m_pRightInsertBtn->setEnabled(true); m_pRightDeleteBtn->setEnabled(true); // 在初始化时创建图表模式按钮但不显示 createChartModeBtn(); } void nmWxTimeDependentSkin::updateCharts() { if(!m_pPressureChart || !m_pFlowRateChart) { return; } // 同步对齐设置 bool snapToRateChanges = m_pSnapToRateChangesCheckBox->isChecked(); m_pPressureChart->setSnapToRateChanges(snapToRateChanges); m_pFlowRateChart->setSnapToRateChanges(snapToRateChanges); // 更新压力数据 if(!pressurePoints.isEmpty()) { m_pPressureChart->setData(pressurePoints); // 使用本地副本数据而不是从管理器获取 m_pPressureChart->setFlowSegmentData(m_perforationData); } // 更新流量数据 if(!flowPoints.isEmpty()) { m_pFlowRateChart->setData(flowPoints); // 使用本地副本数据而不是从管理器获取 m_pFlowRateChart->setFlowSegmentData(m_perforationData); } } void nmWxTimeDependentSkin::updateTableFromFlowSegments() { if(!m_pDataTable) return; const QVector& segments = m_perforationData.getFlowSegments(); const bool enableRateDep = m_perforationData.isRateDependentEnabled(); const int requiredCols = enableRateDep ? 4 : 3; // 只有在列数真正改变时才重建表格结构 bool needRefreshStructure = (m_pDataTable->columnCount() != requiredCols); if(needRefreshStructure) { refreshTableStructure(); } else { // 确保单位行存在但不重建ComboBox ensureHeaderRowExists(); // 更新时间单位显示 updateTimeUnitComboVisibility(); } // 屏蔽信号,防止填充过程中触发 bool oldTblBlocked = m_pDataTable->blockSignals(true); bool oldTimeBlocked = false; bool oldQBlocked = false; if(m_pTimeUnitCombo) oldTimeBlocked = m_pTimeUnitCombo->blockSignals(true); if(m_pDSdQUnitCombo) oldQBlocked = m_pDSdQUnitCombo->blockSignals(true); // 设置总行数 m_pDataTable->setRowCount(segments.size() + 1); QStringList rowLabels; rowLabels << ""; // 填充数据行(从第1行开始,第0行是单位行) for(int i = 0; i < segments.size(); ++i) { const int row = i + 1; rowLabels << QString::number(row); const FlowSegmentData& segment = segments[i]; // 更新选择箭头 QTableWidgetItem* arrowItem = m_pDataTable->item(row, 0); if(!arrowItem) { arrowItem = new QTableWidgetItem(); arrowItem->setFlags(Qt::ItemIsEnabled); m_pDataTable->setItem(row, 0, arrowItem); } arrowItem->setIcon(segment.isSelected ? createTriangleIcon() : QIcon()); arrowItem->setText(""); // 更新时间 double startHr = segment.segmentStart.getValue().toDouble(); QString timeDisplay = formatTimeDisplay(startHr, i); QTableWidgetItem* timeItem = m_pDataTable->item(row, 1); if(!timeItem) { timeItem = new QTableWidgetItem(); timeItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); // 如果显示日期,时间列设为只读 bool showDates = m_perforationData.isShowDatesEnabled(); Qt::ItemFlags flags = Qt::ItemIsEnabled; if(!showDates && i > 0) { flags |= Qt::ItemIsEditable; } timeItem->setFlags(flags); m_pDataTable->setItem(row, 1, timeItem); } timeItem->setText(timeDisplay); // 更新Skin值 double skinValue = segment.skinValue.getValue().toDouble(); QTableWidgetItem* skinItem = m_pDataTable->item(row, 2); if(!skinItem) { skinItem = new QTableWidgetItem(); skinItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); skinItem->setFlags(Qt::ItemIsEnabled | Qt::ItemIsEditable); m_pDataTable->setItem(row, 2, skinItem); } skinItem->setText(QString::number(skinValue, 'f', 5)); // 更新dS/dQ值(如果启用) if(enableRateDep) { double base = segment.dSdQ.getValue().toDouble(); double disp = convertDSdQ(base, "1/B/D", m_dSdQDisplayUnit); QTableWidgetItem* dsdqItem = m_pDataTable->item(row, 3); if(!dsdqItem) { dsdqItem = new QTableWidgetItem(); dsdqItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); dsdqItem->setFlags(Qt::ItemIsEnabled | Qt::ItemIsEditable); m_pDataTable->setItem(row, 3, dsdqItem); } // 不同流量单位的倍率差异较大,保留有效位以免小系数显示为零。 dsdqItem->setText(QString::number(disp, 'g', 15)); } } // 第二页隐藏流量相关列;保留数据,返回第一页时恢复显示。 if(enableRateDep) m_pDataTable->setColumnHidden(3, m_pChartWidget != nullptr); // 表格重建或增删分段后同步行号,第零行仅显示单位。 m_pDataTable->setVerticalHeaderLabels(rowLabels); // 恢复信号状态 if(m_pTimeUnitCombo) m_pTimeUnitCombo->blockSignals(oldTimeBlocked); if(m_pDSdQUnitCombo) m_pDSdQUnitCombo->blockSignals(oldQBlocked); m_pDataTable->blockSignals(oldTblBlocked); // 仅单射孔且至少有两个表皮分段时,允许进入表皮系数与流量页面。 m_pSkinVsRateBtn->setEnabled(m_currentWell && m_currentWell->getPerforationCount() == 1 && segments.size() >= 2); } void nmWxTimeDependentSkin::ensureHeaderRowExists() { // 确保第0行存在,但不重建ComboBox if(m_pDataTable->rowCount() == 0) { m_pDataTable->insertRow(0); m_pDataTable->setRowHeight(0, 24); } // 如果ComboBox不存在才创建(只在首次或结构改变时) if(!m_pTimeUnitCombo) { createTimeUnitCombo(); } const bool enableRateDep = m_perforationData.isRateDependentEnabled(); if(enableRateDep && !m_pDSdQUnitCombo) { createDSdQUnitCombo(); } else if(!enableRateDep && m_pDSdQUnitCombo) { // 如果禁用了rate dependent,删除dS/dQ ComboBox m_pDSdQUnitCombo->disconnect(this); delete m_pDSdQUnitCombo; m_pDSdQUnitCombo = nullptr; } } void nmWxTimeDependentSkin::createTimeUnitCombo() { bool showDates = m_perforationData.isShowDatesEnabled(); if(showDates) { // 如果显示日期,创建一个标签而不是下拉框 QLabel* dateTimeLabel = new QLabel(tr("Date Time"), m_pDataTable); dateTimeLabel->setAlignment(Qt::AlignCenter); dateTimeLabel->setStyleSheet("QLabel { color: #333; font-size: 12px; }"); m_pDataTable->setCellWidget(0, 1, dateTimeLabel); return; } // 单位列表由底层配置提供,旧的 hr 名称映射到配置中的小时单位。 m_pTimeUnitCombo = new QComboBox(m_pDataTable); iUnitGroup* group = iUnitHelper::getUnitGroupByUnit("s"); const QStringList units = group ? group->getAllUnitNames() : (QStringList() << "hr"); if(m_timeDisplayUnit == "hr" && units.contains("h")) m_timeDisplayUnit = "h"; int idx = units.indexOf(m_timeDisplayUnit); m_pTimeUnitCombo->addItems(units); m_pTimeUnitCombo->setCurrentIndex(idx >= 0 ? idx : 0); m_timeDisplayUnit = m_pTimeUnitCombo->currentText(); m_pTimeUnitCombo->setEditable(true); m_pTimeUnitCombo->lineEdit()->setAlignment(Qt::AlignCenter); m_pTimeUnitCombo->lineEdit()->setReadOnly(true); m_pTimeUnitCombo->setStyleSheet( "QComboBox { border: none; background: transparent; padding-left: 4px; }" "QComboBox::drop-down { border: none; }" ); m_pDataTable->setCellWidget(0, 1, m_pTimeUnitCombo); connect(m_pTimeUnitCombo, SIGNAL(currentIndexChanged(const QString &)), this, SLOT(onTimeUnitChanged(const QString &))); } void nmWxTimeDependentSkin::createDSdQUnitCombo() { // 复用底层流量单位组,显示为整个流量单位的倒数。 m_pDSdQUnitCombo = new QComboBox(m_pDataTable); iUnitGroup* group = iUnitHelper::getUnitGroupByUnit("stb/d"); QStringList qUnits; if(group) { const QStringList units = group->getAllUnitNames(); for(int i = 0; i < units.size(); ++i) qUnits << "1/(" + units[i] + ")"; } if(qUnits.isEmpty()) qUnits << "1/B/D"; m_pDSdQUnitCombo->addItems(qUnits); if(m_dSdQDisplayUnit == "1/B/D") m_dSdQDisplayUnit = "1/(stb/d)"; int qIdx = qUnits.indexOf(m_dSdQDisplayUnit); m_pDSdQUnitCombo->setCurrentIndex(qIdx >= 0 ? qIdx : 0); m_dSdQDisplayUnit = m_pDSdQUnitCombo->currentText(); m_pDSdQUnitCombo->setStyleSheet( "QComboBox { border: none; background: transparent; padding-left: 4px; }" "QComboBox::drop-down { border: none; }" ); m_pDataTable->setCellWidget(0, 3, m_pDSdQUnitCombo); connect(m_pDSdQUnitCombo, SIGNAL(currentIndexChanged(const QString&)), this, SLOT(onDSdQUnitChanged(const QString&))); } void nmWxTimeDependentSkin::onFlowSegmentSelected(int segmentIndex) { // 如果在移除模式,执行合并操作 if(m_isRemoveMode) { // 尝试与左侧段合并 bool success = mergeSegmentWithPrevious(segmentIndex); // 无论成功与否,都退出移除模式 m_isRemoveMode = false; if(success) { // 如果成功,更新图表和表格显示 updateCharts(); updateTableFromFlowSegments(); } return; } // 如果在添加模式,不处理段选择 if(m_isAddMode) { return; } // 正常的选择流动段 selectFlowSegment(segmentIndex); // 更新图表显示 updateCharts(); // 更新表格显示 updateTableFromFlowSegments(); } bool nmWxTimeDependentSkin::addFlowSegmentAtExactTime(double timePoint) { // 找到包含该时间点的流动段 int segmentIndex = m_perforationData.findFlowSegmentByTime(timePoint); if(segmentIndex < 0) { return false; // 没有找到对应的流动段 } // 直接在该时间点分割流动段 return splitFlowSegmentAtTime(segmentIndex, timePoint); } void nmWxTimeDependentSkin::onChartPointClicked(double timePoint) { if(!m_isAddMode) { return; // 不在添加模式,忽略此事件 } bool success = false; // 检查"Snap to rate changes"复选框状态 bool snapToRateChanges = m_pSnapToRateChangesCheckBox->isChecked(); if(snapToRateChanges) { // 勾选状态:使用现有逻辑,在点击位置所在阶梯段的结束边界添加流动段 success = addFlowSegmentAtTime(timePoint); } else { // 未勾选状态:直接在点击位置分割流动段 success = addFlowSegmentAtExactTime(timePoint); } // 无论成功与否,都退出添加模式 m_isAddMode = false; if(success) { // 如果成功,更新图表和表格显示 updateCharts(); updateTableFromFlowSegments(); } } void nmWxTimeDependentSkin::onAddClicked() { if(m_isAddMode) { // 如果已经在添加模式,退出添加模式 m_isAddMode = false; } else { // 进入添加模式,同时退出移除模式 m_isAddMode = true; m_isRemoveMode = false; } } void nmWxTimeDependentSkin::onRemoveClicked() { if(m_isRemoveMode) { // 如果已经在移除模式,退出移除模式 m_isRemoveMode = false; } else { // 进入移除模式,同时退出添加模式 m_isRemoveMode = true; m_isAddMode = false; } } void nmWxTimeDependentSkin::onSplitStepsClicked() { // 一次性分割所有流动段 splitAllFlowSegments(); // 更新图表显示 updateCharts(); // 更新表格数据 updateTableFromFlowSegments(); } QString nmWxTimeDependentSkin::skinComputeUnavailableReason() const { // 多井按井 UUID 查找各自的冻结结果,观察井没有可用于表皮反算的自身流量。 nmDataAnalyzeManager* manager = m_pDataManager.data(); if(!manager || !m_currentWell) return tr("Select a well with valid model results before estimating skin."); if(m_currentWell->getPerforationCount() != 1) return tr("Skin estimation requires a single perforation."); if(!m_perforationData.getTimeValues().isEmpty()) return tr("Skin estimation does not yet support perforation closing intervals."); if(!manager->isPebiResultSnapshotCurrent()) return tr("Generate a model with the current inputs and a constant skin before computing skin steps."); QSharedPointer snapshot = manager->getPebiResultSnapshot(); const nmPebiResultWellSnapshot* well = snapshot->findWell(m_currentWell->getWellInstanceId()); if(!well || !well->m_bHasSkin || well->m_sGaugeInputSha1.isEmpty() || well->m_sGaugeInputSha1 != m_currentWell->calculateGaugeInputSha1()) return tr("The model result does not match this well's pressure and rate data. Generate the model again."); if(well->m_eWellMode != NM_CaseWell_RateControlled) return tr("Observation wells have no controlled rate for skin estimation."); // 直井、压裂井和多段压裂井共用模型压力与井总流量,估算整井等效表皮。 if(!validSkinPressureCurve(well->m_oCurves.m_vecResultPressure) || m_currentWell->getPressurePoints().size() < 2) return tr("Skin estimation requires model and measured pressure curves."); return QString(); } void nmWxTimeDependentSkin::updateComputeState() { // 仅更新计算按钮的可用状态,切换页面不改变原有模型设置。 const QString reason = skinComputeUnavailableReason(); m_pComputeBtn->setEnabled(reason.isEmpty() && !m_pChartWidget); } void nmWxTimeDependentSkin::onComputeClicked() { const QString reason = skinComputeUnavailableReason(); if(!reason.isEmpty()) { updateComputeState(); QMessageBox::information(this, tr("Compute"), reason); return; } QSharedPointer snapshot = m_pDataManager->getPebiResultSnapshot(); const nmPebiResultWellSnapshot* well = snapshot->findWell(m_currentWell->getWellInstanceId()); const nmPebiResultPvtParameters& pvt = snapshot->getPvtParameters(); const int modelType = snapshot->getSolverSettings().m_nSolverModelType; // 每次点击从所属分析重新读取参考单值,不回退到旧快照或默认物性。 QStringList names; if(modelType == SMT_Oil_ConstPvt || modelType == SMT_Oil_VariablePvt || modelType == SMT_Oil_Water_TwoPhase) names << "Bo" << "Miuo"; if(modelType == SMT_Water_ConstPvt || modelType == SMT_Water_VariablePvt) names << "Bw" << "Miuw"; else if(modelType == SMT_Oil_Water_TwoPhase) names << "Bw"; if(modelType == SMT_Gas_VariablePvt) names << "Bg" << "Miug"; QMap referencePvt; if(!names.isEmpty() && !m_pDataManager->getAnalysisPvtValues(names, referencePvt)) { QMessageBox::information(this, tr("Compute"), tr("Cannot read reference PVT values from this analysis.")); return; } SkinComputeData data; const QString preparationError = prepareSkinCompute(*well, snapshot->getReservoirParameters(), pvt, modelType, referencePvt, data); if(!preparationError.isEmpty()) { QMessageBox::information(this, tr("Compute"), preparationError); return; } QVector > measured(2); const QVector points = m_currentWell->getPressurePoints(); for(int i = 0; i < points.size(); ++i) { measured[0].append(points[i].x()); measured[1].append(points[i].y()); } // 只更新本窗口副本;计算后移除无匹配流量段的分界,取消仍可放弃本次估算。 QVector segments = m_perforationData.getFlowSegments(); const int originalSegmentCount = segments.size(); const int computedCount = estimateSkinSegments(*well, data, measured, segments); if(computedCount == 0 && segments.size() == originalSegmentCount) { QMessageBox::information(this, tr("Compute"), tr("No skin steps could be estimated. Check pressure data, non-zero rate steps and the gas PVT pressure range.")); return; } // 分段删除后重新定位选中项,避免旧索引指向其他行或越界;空结果由表格刷新清除旧行。 int selectedIndex = 0; if(segments.size() != originalSegmentCount && !segments.isEmpty()) { for(int i = 0; i < segments.size(); ++i) { if(segments[i].isSelected) { selectedIndex = i; break; } } for(int i = 0; i < segments.size(); ++i) segments[i].isSelected = i == selectedIndex; } m_perforationData.setFlowSegments(segments); if(segments.size() != originalSegmentCount && !segments.isEmpty()) m_perforationData.setSelectedSegmentIndex(selectedIndex); // 原来的表皮/流量拟合线已不对应新估算值,下次进入第二页时重新拟合。 m_perforationData.setLinePositions(-999.0, -999.0, -999.0, -999.0); updateCharts(); updateTableFromFlowSegments(); } void nmWxTimeDependentSkin::onPerforationClosingBtnClicked() { nmWxPerforationClosing* dlg = new nmWxPerforationClosing(m_currentWell, this); // 子窗口确认后同步封堵字段,防止主窗口随后提交旧副本覆盖这些修改。 connect(dlg, SIGNAL(accepted()), this, SLOT(onPerforationClosingAccepted())); dlg->show(); } void nmWxTimeDependentSkin::onPerforationClosingAccepted() { if(!m_currentWell) return; const nmDataPerforation data = m_currentWell->getPerforationCopy(m_currentPerforationIndex); // 仅同步封堵数据,保留当前表皮编辑值。 m_perforationData.setTimeValues(data.getTimeValues()); m_perforationData.setTimeStates(data.getTimeStates()); m_perforationData.setBaseTime(data.getBaseTime()); updateComputeState(); } void nmWxTimeDependentSkin::onSkinVsRateClicked() { // 如果已经显示了图表,就恢复原始状态 if(m_pChartWidget != nullptr) { restoreOriginalCharts(); restoreToolbarState(); return; } // 创建交互式图表 showInteractiveChart(); hideToolbarButtons(); } void nmWxTimeDependentSkin::showInteractiveChart() { if(m_pChartSplitter == nullptr) return; // 隐藏分割器 m_pChartSplitter->hide(); // 创建交互式图表widget m_pChartWidget = new nmWxChartWidget(this); // 切换模式后沿用压力图的左侧扩展宽度,保持绘图区位置不变。 const int chartLeftExtension = m_pPressureChart->contentsMargins().left(); // 将图表与下拉框之间的空白交给横轴标题,绘图区补偿相同高度。 QLayout* leftLayout = m_pComboBox->parentWidget()->layout(); const int chartBottomExtension = qMax(0, leftLayout->spacing()); leftLayout->setSpacing(0); m_pChartWidget->setContentsMargins(chartLeftExtension, 0, 0, chartBottomExtension); m_pChartWidget->setMinimumSize(400 + chartLeftExtension, 400 + chartBottomExtension); // 连接信号槽 connect(m_pChartWidget, SIGNAL(skinValuesChanged(double, double)), this, SLOT(onSkinValuesChanged(double, double))); // 连接过滤完成信号 connect(m_pChartWidget, SIGNAL(crossMarksFiltered()), this, SLOT(onCrossMarksFiltered())); // 添加到布局 m_pChartLayout->addWidget(m_pChartWidget, 1); m_pChartWidget->show(); // 第二页不使用流量变化点对齐,第一页的设置仍保留在数据中。 m_pChartWidget->setSnapToRateChanges(false); m_pChartWidget->setFlowSegmentData(m_perforationData); // 传递原始流量数据 if(m_currentWell != nullptr) { QVector rawFlowData = m_currentWell->getFlowPoints(); m_pChartWidget->setRawFlowData(rawFlowData); } m_pChartWidget->calculateDataRange(); // 在数据传递完成后,检查是否需要加载已保存的线条位置 // 这里会被延迟执行,确保初始拟合先完成 QTimer::singleShot(10, this, SLOT(loadSavedLinePositionsIfValid())); // 初始化编辑框 updateEditBoxesFromData(); } void nmWxTimeDependentSkin::loadSavedLinePositionsIfValid() { if(m_pChartWidget == nullptr) return; // 直接检查数据类中是否有有效的线条位置 double x1, y1, x2, y2; m_perforationData.getLinePositions(x1, y1, x2, y2); // 检查是否为有效数据(-999表示未设置,或者是默认值100,0,200,0) bool hasValidData = (x1 != -999.0 && y1 != -999.0 && x2 != -999.0 && y2 != -999.0); // 额外检查:如果是默认值(100,0,200,0),也认为是无效数据 bool isDefaultValue = (qAbs(x1 - 100.0) < 1e-6 && qAbs(y1 - 0.0) < 1e-6 && qAbs(x2 - 200.0) < 1e-6 && qAbs(y2 - 0.0) < 1e-6); if(hasValidData && !isDefaultValue) { // 有有效且非默认的数据,加载保存的位置(用户之前调整过的位置) loadChartPositionsFromData(); updateEditBoxesFromData(); } else { // 没有有效数据或是默认值,图表组件的初始拟合结果已经生效 // 不需要再次保存,因为 onSkinValuesChanged 已经保存过了 updateEditBoxesFromData(); } } void nmWxTimeDependentSkin::initializeChartFromData() { if(m_pChartWidget == nullptr) return; // 从 m_skinVsRateData 获取当前值 bool skin0Ok, dSkinDqOk; double skin0Value = m_perforationData.getSkin0().getValue().toString().toDouble(&skin0Ok); double dSkinDqValue = m_perforationData.getdSkindq().getValue().toString().toDouble(&dSkinDqOk); // 如果数据无效,使用默认值 if(!skin0Ok) { skin0Value = 0.0; } if(!dSkinDqOk) { dSkinDqValue = 0.0; } // 设置图表的初始值和线条位置 m_pChartWidget->setSkinValues(skin0Value, dSkinDqValue); } void nmWxTimeDependentSkin::loadChartPositionsFromData() { if(m_pChartWidget == nullptr) return; // 获取保存的线条位置 double x1, y1, x2, y2; m_perforationData.getLinePositions(x1, y1, x2, y2); // 检查是否有有效数据(-999表示未设置) if(x1 == -999.0 || y1 == -999.0 || x2 == -999.0 || y2 == -999.0) { // 没有有效数据,不执行加载 return; } // 设置图表的线条位置 m_pChartWidget->setLinePositions(x1, y1, x2, y2); } void nmWxTimeDependentSkin::saveChartPositionsToData() { if(m_pChartWidget == nullptr) return; // 获取当前图表的线条位置 double x1, y1, x2, y2; m_pChartWidget->getLinePositions(x1, y1, x2, y2); // 保存到数据对象 m_perforationData.setLinePositions(x1, y1, x2, y2); // 同时更新skin0和dSkin/dq值 double skin0Value = m_pChartWidget->getSkin0Value(); double dSkinDqValue = m_pChartWidget->getDSkinDqValue(); m_perforationData.getSkin0().setValue(QString::number(skin0Value, 'f', 5)); m_perforationData.getdSkindq().setValue(QString::number(dSkinDqValue, 'f', 5)); } void nmWxTimeDependentSkin::onSkinValuesChanged(double skin0Value, double dSkinDqValue) { // 实时保存图表位置到数据对象 saveChartPositionsToData(); // 更新编辑框显示 updateEditBoxesFromData(); } void nmWxTimeDependentSkin::onCrossMarksFiltered() { // 过滤完成后,自动保存图表位置 saveChartPositionsToData(); // 更新编辑框显示 updateEditBoxesFromData(); } void nmWxTimeDependentSkin::updateEditBoxesFromData() { if(m_pSkinEdit && m_pDSkinDqEdit) { QString skin0Text = m_perforationData.getSkin0().getValue().toString(); QString dSkinDqText = m_perforationData.getdSkindq().getValue().toString(); m_pSkinEdit->setText(skin0Text); m_pDSkinDqEdit->setText(dSkinDqText); } } void nmWxTimeDependentSkin::restoreOriginalCharts() { if(m_pChartLayout == nullptr || m_pChartSplitter == nullptr) return; // 如果图表widget存在且处于套索模式,先退出 if(m_pChartWidget != nullptr) { if(m_pChartWidget->getLassoMode()) { m_pChartWidget->setLassoMode(false); } } // 移除交互式图表 if(m_pChartWidget != nullptr) { // 切回压力/流量图时恢复原来的布局间距。 m_pComboBox->parentWidget()->layout()->setSpacing(m_pChartWidget->contentsMargins().bottom()); m_pChartLayout->removeWidget(m_pChartWidget); m_pChartWidget->deleteLater(); m_pChartWidget = nullptr; } // 重新显示分割器(包含原始的两个图表) m_pChartSplitter->show(); // 确保原始图表在分割器中是可见的 if(m_pOriginalPressureChart != nullptr) { m_pOriginalPressureChart->show(); } if(m_pOriginalFlowRateChart != nullptr) { m_pOriginalFlowRateChart->show(); } // 恢复默认比例 (1:1) m_pChartSplitter->setStretchFactor(0, 1); m_pChartSplitter->setStretchFactor(1, 1); } void nmWxTimeDependentSkin::hideToolbarButtons() { // 通过按钮的父容器隐藏原有按钮 if(m_pAddBtn && m_pAddBtn->parentWidget()) { m_pAddBtn->parentWidget()->setVisible(false); } if(m_pRemoveBtn && m_pRemoveBtn->parentWidget()) { m_pRemoveBtn->parentWidget()->setVisible(false); } if(m_pSplitStepsBtn && m_pSplitStepsBtn->parentWidget()) { m_pSplitStepsBtn->parentWidget()->setVisible(false); } if(m_pComputeBtn && m_pComputeBtn->parentWidget()) { m_pComputeBtn->parentWidget()->setVisible(false); } if(m_pPerforationClosingBtn && m_pPerforationClosingBtn->parentWidget()) { m_pPerforationClosingBtn->parentWidget()->setVisible(false); } // 显示图表模式下的新按钮 if(m_pFilterContainer) { m_pFilterContainer->setVisible(true); } if(m_pResetFilterContainer) { m_pResetFilterContainer->setVisible(true); } if(m_pResetLinesContainer) { m_pResetLinesContainer->setVisible(true); } // 显示编辑框容器 if(m_pEditContainer) { m_pEditContainer->setVisible(true); } // 仅改变第二页的控件状态,屏蔽信号以保留第一页设置及原有dS/dQ数据。 bool snapBlocked = m_pSnapToRateChangesCheckBox->blockSignals(true); m_pSnapToRateChangesCheckBox->setChecked(false); m_pSnapToRateChangesCheckBox->setEnabled(false); m_pSnapToRateChangesCheckBox->blockSignals(snapBlocked); bool rateBlocked = m_pAddRateDependentCheckBox->blockSignals(true); m_pAddRateDependentCheckBox->setChecked(false); m_pAddRateDependentCheckBox->setEnabled(false); m_pAddRateDependentCheckBox->blockSignals(rateBlocked); updateTableFromFlowSegments(); } void nmWxTimeDependentSkin::restoreToolbarState() { // 显示所有隐藏的按钮容器 if(m_pAddBtn && m_pAddBtn->parentWidget()) { m_pAddBtn->parentWidget()->setVisible(true); } if(m_pRemoveBtn && m_pRemoveBtn->parentWidget()) { m_pRemoveBtn->parentWidget()->setVisible(true); } if(m_pSplitStepsBtn && m_pSplitStepsBtn->parentWidget()) { m_pSplitStepsBtn->parentWidget()->setVisible(true); } // 计算入口仅在默认的表皮分段页面显示。 if(m_pComputeBtn && m_pComputeBtn->parentWidget()) { m_pComputeBtn->parentWidget()->setVisible(true); updateComputeState(); } if(m_pPerforationClosingBtn && m_pPerforationClosingBtn->parentWidget()) { m_pPerforationClosingBtn->parentWidget()->setVisible(true); } // 隐藏图表模式下的按钮 if(m_pFilterContainer) { m_pFilterContainer->setVisible(false); } if(m_pResetFilterContainer) { m_pResetFilterContainer->setVisible(false); } if(m_pResetLinesContainer) { m_pResetLinesContainer->setVisible(false); } // 隐藏编辑框容器 if(m_pEditContainer) { m_pEditContainer->setVisible(false); } // 从保留的数据恢复第一页设置,不触发重新吸附或清零流量相关系数。 bool snapBlocked = m_pSnapToRateChangesCheckBox->blockSignals(true); m_pSnapToRateChangesCheckBox->setChecked(m_perforationData.isSnapToRateChangesEnabled()); m_pSnapToRateChangesCheckBox->setEnabled(true); m_pSnapToRateChangesCheckBox->blockSignals(snapBlocked); bool rateBlocked = m_pAddRateDependentCheckBox->blockSignals(true); m_pAddRateDependentCheckBox->setChecked(m_perforationData.isRateDependentEnabled()); m_pAddRateDependentCheckBox->setEnabled(true); m_pAddRateDependentCheckBox->blockSignals(rateBlocked); updateCharts(); // 刷新表格显示 updateTableFromFlowSegments(); } void nmWxTimeDependentSkin::createChartModeBtn() { // 图表模式也通过框架加载图标,与普通模式保持一致。 // 创建Filter按钮 m_pFilterContainer = new QWidget(this); QVBoxLayout* filterLayout = new QVBoxLayout(m_pFilterContainer); filterLayout->setSpacing(0); filterLayout->setContentsMargins(2, 2, 2, 2); m_pFilterBtn = new QPushButton(m_pFilterContainer); m_pFilterBtn->setIcon(zxLoadIcon("NmFilter")); m_pFilterBtn->setIconSize(QSize(36, 36)); m_pFilterBtn->setFixedSize(40, 40); m_pFilterBtn->setEnabled(true); QLabel* filterLabel = new QLabel(tr("Filter"), m_pFilterContainer); filterLabel->setAlignment(Qt::AlignCenter); filterLayout->addWidget(m_pFilterBtn, 0, Qt::AlignHCenter); filterLayout->addWidget(filterLabel); m_pFilterContainer->setMinimumWidth(100); m_pFilterContainer->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); m_pFilterContainer->setVisible(false); m_pToolbarLayout->insertWidget(1, m_pFilterContainer); // 创建Reset Filter按钮 m_pResetFilterContainer = new QWidget(this); QVBoxLayout* resetFilterLayout = new QVBoxLayout(m_pResetFilterContainer); resetFilterLayout->setSpacing(0); resetFilterLayout->setContentsMargins(2, 2, 2, 2); m_pResetFilterBtn = new QPushButton(m_pResetFilterContainer); m_pResetFilterBtn->setIcon(zxLoadIcon("NmResetFilter")); m_pResetFilterBtn->setIconSize(QSize(36, 36)); m_pResetFilterBtn->setFixedSize(40, 40); m_pResetFilterBtn->setEnabled(true); QLabel* resetFilterLabel = new QLabel(tr("Reset Filter"), m_pResetFilterContainer); resetFilterLabel->setAlignment(Qt::AlignCenter); resetFilterLayout->addWidget(m_pResetFilterBtn, 0, Qt::AlignHCenter); resetFilterLayout->addWidget(resetFilterLabel); m_pResetFilterContainer->setMinimumWidth(100); m_pResetFilterContainer->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); m_pResetFilterContainer->setVisible(false); m_pToolbarLayout->insertWidget(2, m_pResetFilterContainer); // 创建Reset Lines按钮 m_pResetLinesContainer = new QWidget(this); QVBoxLayout* resetLinesLayout = new QVBoxLayout(m_pResetLinesContainer); resetLinesLayout->setSpacing(0); resetLinesLayout->setContentsMargins(2, 2, 2, 2); m_pResetLinesBtn = new QPushButton(m_pResetLinesContainer); m_pResetLinesBtn->setIcon(zxLoadIcon("NmResetLines")); m_pResetLinesBtn->setIconSize(QSize(36, 36)); m_pResetLinesBtn->setFixedSize(40, 40); QLabel* resetLinesLabel = new QLabel(tr("Reset Lines"), m_pResetLinesContainer); resetLinesLabel->setAlignment(Qt::AlignCenter); resetLinesLayout->addWidget(m_pResetLinesBtn, 0, Qt::AlignHCenter); resetLinesLayout->addWidget(resetLinesLabel); m_pResetLinesContainer->setMinimumWidth(100); m_pResetLinesContainer->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); m_pResetLinesContainer->setVisible(false); m_pToolbarLayout->insertWidget(3, m_pResetLinesContainer); // 创建编辑框容器 createEditBoxes(); // 连接信号槽(暂时为空实现) connect(m_pFilterBtn, SIGNAL(clicked()), this, SLOT(onFilterClicked())); connect(m_pResetFilterBtn, SIGNAL(clicked()), this, SLOT(onResetFilterClicked())); connect(m_pResetLinesBtn, SIGNAL(clicked()), this, SLOT(onResetLinesClicked())); } QVector nmWxTimeDependentSkin::processFlowData(const QVector& rawFlowData) { QVector processedData; if(rawFlowData.isEmpty()) { return processedData; } double totalTime = 0.0; // 预期的阶梯图应该是: // 每个时间段内保持恒定流量,在时间段边界处跳跃到新流量值 for(int i = 0; i < rawFlowData.size(); ++i) { const QPointF& currentSegment = rawFlowData[i]; if(i == 0) { // 第一个数据点 - 设置初始流量 processedData.append(QPointF(totalTime, currentSegment.y())); } // 添加时间间隔到总时间 totalTime += currentSegment.x(); // 添加当前时间段结束时的点(保持当前流量) processedData.append(QPointF(totalTime, currentSegment.y())); // 如果有下一个时间段且流量不同,添加垂直跳跃 if(i < rawFlowData.size() - 1) { double nextFlowRate = rawFlowData[i + 1].y(); if(qAbs(nextFlowRate - currentSegment.y()) > 1e-6) { processedData.append(QPointF(totalTime, nextFlowRate)); } } } return processedData; } void nmWxTimeDependentSkin::createEditBoxes() { // 创建一个容器来垂直排布编辑框 QWidget* editContainer = new QWidget(this); QVBoxLayout* editLayout = new QVBoxLayout(editContainer); editLayout->setSpacing(3); editLayout->setContentsMargins(8, 8, 8, 8); // 创建第一行:Skin标签和编辑框 QHBoxLayout* skinLayout = new QHBoxLayout(); skinLayout->setSpacing(5); QLabel* skinLabel = new QLabel(tr("Skin0:"), editContainer); skinLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); skinLabel->setFixedWidth(60); skinLabel->setStyleSheet("QLabel { color: #333; font-size: 12px; }"); m_pSkinEdit = new QLineEdit(editContainer); m_pSkinEdit->setText("0.00000"); m_pSkinEdit->setFixedSize(100, 22); m_pSkinEdit->setReadOnly(true); // 设置为只读 m_pSkinEdit->setStyleSheet( "QLineEdit {" "border: 1px solid #aaa;" "border-radius: 2px;" "padding: 2px 4px;" "background-color: #f5f5f5;" // 只读背景色 "font-size: 12px;" "color: #333;" "}" ); skinLayout->addWidget(skinLabel); skinLayout->addWidget(m_pSkinEdit); skinLayout->addStretch(); // 创建第二行:dSkin/dq标签和编辑框 QHBoxLayout* dSkinLayout = new QHBoxLayout(); dSkinLayout->setSpacing(5); QLabel* dSkinLabel = new QLabel(tr("dSkin/dq:"), editContainer); dSkinLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); dSkinLabel->setFixedWidth(60); dSkinLabel->setStyleSheet("QLabel { color: #333; font-size: 12px; }"); m_pDSkinDqEdit = new QLineEdit(editContainer); m_pDSkinDqEdit->setText("0.00000"); m_pDSkinDqEdit->setFixedSize(100, 22); m_pDSkinDqEdit->setReadOnly(true); // 设置为只读 m_pDSkinDqEdit->setStyleSheet( "QLineEdit {" "border: 1px solid #aaa;" "border-radius: 2px;" "padding: 2px 4px;" "background-color: #f5f5f5;" // 只读背景色 "font-size: 12px;" "color: #333;" "}" ); dSkinLayout->addWidget(dSkinLabel); dSkinLayout->addWidget(m_pDSkinDqEdit); dSkinLayout->addStretch(); // 将两行添加到垂直布局 editLayout->addLayout(skinLayout); editLayout->addLayout(dSkinLayout); // 设置容器大小 editContainer->setMinimumWidth(180); editContainer->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); editContainer->setStyleSheet( "QWidget {" "background-color: transparent;" "border: none;" "}" ); // 将容器添加到工具栏布局,在复选框之前 int checkboxIndex = m_pToolbarLayout->count() - 1; m_pToolbarLayout->insertWidget(checkboxIndex, editContainer); // 保存引用以便后续控制显示/隐藏 m_pEditContainer = editContainer; m_pSkinLabel = skinLabel; m_pDSkinLabel = dSkinLabel; // 初始时隐藏 editContainer->setVisible(false); } QIcon nmWxTimeDependentSkin::createTriangleIcon() { QPixmap trianglePixmap(16, 16); trianglePixmap.fill(Qt::transparent); QPainter painter(&trianglePixmap); painter.setRenderHint(QPainter::Antialiasing, true); // 绘制小三角形(指向右的箭头) QPolygon triangle; triangle << QPoint(4, 6) << QPoint(4, 10) << QPoint(10, 8); painter.setBrush(QBrush(QColor(0, 0, 0))); // 黑色三角形 painter.setPen(QPen(QColor(0, 0, 0), 1)); painter.drawPolygon(triangle); return QIcon(trianglePixmap); } void nmWxTimeDependentSkin::onTableDataChanged(int row, int column) { if(row == 0) return; // 如果显示日期模式,禁止编辑时间列 if(column == 1 && m_perforationData.isShowDatesEnabled()) { // 恢复原值 QVector segments = m_perforationData.getFlowSegments(); int segRow = row - 1; if(segRow >= 0 && segRow < segments.size()) { double startHr = segments[segRow].segmentStart.getValue().toDouble(); QString timeDisplay = formatTimeDisplay(startHr, segRow); QTableWidgetItem* item = m_pDataTable->item(row, column); if(item) { item->setText(timeDisplay); } } return; } QVector segments = m_perforationData.getFlowSegments(); int segRow = row - 1; if(segRow < 0 || segRow >= segments.size()) return; QTableWidgetItem* item = m_pDataTable->item(row, column); if(!item) return; if(column == 1) { if(segRow == 0) { // 第一个段不允许改:按“秒量化”后的原值回显 double origHr = segments[segRow].segmentStart.getValue().toDouble(); double origSec = toSeconds(origHr, "hr"); double origDisp = fromSeconds(origSec, m_timeDisplayUnit); item->setText(QString::number(origDisp, 'f', 5)); return; } bool ok = false; double newStartDisp = item->text().toDouble(&ok); if(!ok || newStartDisp < 0) { double origHr = segments[segRow].segmentStart.getValue().toDouble(); double origSec = toSeconds(origHr, "hr"); double origDisp = fromSeconds(origSec, m_timeDisplayUnit); item->setText(QString::number(origDisp, 'f', 5)); return; } // —— 把用户输入的“显示单位”量值 -> 秒,并四舍五入到整数秒 —— double newStartSec = toSeconds(newStartDisp, m_timeDisplayUnit); // 邻接边界(以秒为基准)并做最小/最大约束 double minHr = (segRow > 1) ? segments[segRow - 1].segmentStart.getValue().toDouble() + 0.001 : 0.001; double maxHr = (segRow < segments.size() - 1) ? segments[segRow + 1].segmentStart.getValue().toDouble() - 0.001 : m_currentWell->getTotalTimeRange(); double minSec = toSeconds(minHr, "hr"); double maxSec = toSeconds(maxHr, "hr"); if(newStartSec <= minSec || newStartSec >= maxSec) { double origHr = segments[segRow].segmentStart.getValue().toDouble(); double origSec = toSeconds(origHr, "hr"); double origDisp = fromSeconds(origSec, m_timeDisplayUnit); item->setText(QString::number(origDisp, 'f', 5)); return; } // “吸附到流量阶梯边界”的逻辑仍使用小时接口,但最终回到“秒并量化” double finalSec = newStartSec; if(m_pSnapToRateChangesCheckBox->isChecked()) { double guessHr = fromSeconds(newStartSec, "hr"); double snappedHr = m_currentWell->findSmartFlowStepBoundary(guessHr); // 如果吸附点越界,选择最合适的边界 double snappedSec = toSeconds(snappedHr, "hr"); if(snappedSec <= minSec || snappedSec >= maxSec) { QVector boundaries = m_currentWell->getAllFlowStepBoundaries(); double bestSec = newStartSec, minDist = 1e20; for(int i = 0; i < boundaries.size(); ++i) { double bSec = toSeconds(boundaries[i], "hr"); if(bSec > minSec && bSec < maxSec) { double d = qAbs(bSec - newStartSec); if(d < minDist) { minDist = d; bestSec = bSec; } } } snappedSec = bestSec; } finalSec = snappedSec; } // 夹逼(保险) finalSec = qMax(minSec, qMin(maxSec, finalSec)); if(moveFlowSegmentBoundarySec(segRow, finalSec)) { double finalDisp = fromSeconds(finalSec, m_timeDisplayUnit); item->setText(QString::number(finalDisp, 'f', 5)); updateCharts(); updateTableFromFlowSegments(); } else { double origHr = segments[segRow].segmentStart.getValue().toDouble(); double origSec = toSeconds(origHr, "hr"); double origDisp = fromSeconds(origSec, m_timeDisplayUnit); item->setText(QString::number(origDisp, 'f', 5)); } if(m_pChartWidget) m_pChartWidget->regenerateCrossMarkPoints(); return; } if(column == 2) { bool ok; double newSkinValue = item->text().toDouble(&ok); if(!ok) { double originalValue = segments[segRow].skinValue.getValue().toDouble(); item->setText(QString::number(originalValue, 'f', 5)); return; } segments[segRow].skinValue.setValue(QString::number(newSkinValue, 'f', 5)); m_perforationData.setFlowSegments(segments); item->setText(QString::number(newSkinValue, 'f', 5)); if(m_pChartWidget) m_pChartWidget->regenerateCrossMarkPoints(); return; } const bool enableRateDep = m_perforationData.isRateDependentEnabled(); const int dsdqCol = enableRateDep ? 3 : -1; if(enableRateDep && column == dsdqCol) { bool ok = false; double dispVal = item->text().toDouble(&ok); if(!ok) { // 回退显示 double base = segments[segRow].dSdQ.getValue().toDouble(); double disp = convertDSdQ(base, "1/B/D", m_dSdQDisplayUnit); item->setText(QString::number(disp, 'g', 15)); return; } // 转回“内部基准单位 1/B/D”存储 double base = convertDSdQ(dispVal, m_dSdQDisplayUnit, "1/B/D"); // 使用有效位保存,避免换算到内部单位后小系数被截断为零。 segments[segRow].dSdQ.setValue(QString::number(base, 'g', 15)); // 强制其单位为基准(可选:不设置也行,因为我们按基准解读) segments[segRow].dSdQ.setUnit("1/B/D"); // 写回数据类 m_perforationData.setFlowSegments(segments); // 再把显示值(按当前显示单位)回填一次,确保格式 double disp = convertDSdQ(base, "1/B/D", m_dSdQDisplayUnit); item->setText(QString::number(disp, 'g', 15)); return; } } void nmWxTimeDependentSkin::onTableRowClicked(int row, int column) { if(row == 0 || m_isAddMode || m_isRemoveMode) return; int segRow = row - 1; const QVector& segments = m_perforationData.getFlowSegments(); if(segRow < 0 || segRow >= segments.size() || segments[segRow].isSelected) return; selectFlowSegment(segRow); updateCharts(); updateTableFromFlowSegments(); } void nmWxTimeDependentSkin::onRightInsertClicked() { // 获取当前选中的流动段索引(从本地副本) int selectedIndex = m_perforationData.getSelectedSegmentIndex(); const QVector& segments = m_perforationData.getFlowSegments(); // 检查是否有有效的选中段 if(selectedIndex < 0 || selectedIndex >= segments.size()) { return; // 没有选中有效的段 } // 检查是否为第一个段(第一个段之前无法插入) if(selectedIndex == 0) { return; } bool success = false; // 检查"Snap to rate changes"复选框状态 bool snapToRateChanges = m_pSnapToRateChangesCheckBox->isChecked(); if(snapToRateChanges) { // 勾选状态:在前一个段中的流量阶梯边界处插入 success = insertFlowSegmentBeforeSegment(selectedIndex); } else { // 未勾选状态:在前一个段的中间位置插入 success = insertFlowSegmentAtMiddle(selectedIndex); } if(success) { // 更新图表和表格显示 updateCharts(); updateTableFromFlowSegments(); // 如果交互式图表存在,更新×标记 if(m_pChartWidget != nullptr) { m_pChartWidget->regenerateCrossMarkPoints(); } } } void nmWxTimeDependentSkin::onRightAddClicked() { // 检查"Snap to rate changes"复选框状态 bool snapToRateChanges = m_pSnapToRateChangesCheckBox->isChecked(); // 执行添加流动段的操作 addLastFlowSegmentIfMissing(snapToRateChanges); // 更新图表和表格显示 updateCharts(); updateTableFromFlowSegments(); } void nmWxTimeDependentSkin::onRightDeleteClicked() { // 获取当前选中的流动段索引(从本地副本) int selectedIndex = m_perforationData.getSelectedSegmentIndex(); const QVector& segments = m_perforationData.getFlowSegments(); // 检查是否有有效的选中段 if(selectedIndex < 0 || selectedIndex >= segments.size()) { return; // 没有选中有效的段 } // 检查是否为第一个段 if(selectedIndex == 0) { return; } // 检查是否只剩一个段 if(segments.size() <= 1) { return; } // 执行向左合并操作 bool success = mergeSegmentWithPrevious(selectedIndex); if(success) { // 更新图表和表格显示 updateCharts(); updateTableFromFlowSegments(); } } void nmWxTimeDependentSkin::onPerforationChanged(int index) { if(!m_currentWell || index < 0 || index >= m_currentWell->getPerforationCount()) { return; } // 如果选择的是当前射孔段,无需切换 if(index == m_currentPerforationIndex) { return; } // 保存当前射孔段的修改到井数据中 m_currentWell->updatePerforation(m_currentPerforationIndex, m_perforationData); // 切换到新的射孔段 m_currentPerforationIndex = index; m_perforationData = m_currentWell->getPerforationCopy(index); // 如果副本为空,初始化默认数据 if(m_perforationData.getFlowSegments().isEmpty()) { initializeDefaultFlowSegment(); } // 确保副本有默认的表皮系数数据 if(m_perforationData.getSkin0().getValue().toString().isEmpty()) { m_perforationData.getSkin0().setValue("0.0"); } if(m_perforationData.getdSkindq().getValue().toString().isEmpty()) { m_perforationData.getdSkindq().setValue("0.0"); } // 确保线条位置有默认值 double x1, y1, x2, y2; m_perforationData.getLinePositions(x1, y1, x2, y2); if(x1 == -999.0 || y1 == -999.0 || x2 == -999.0 || y2 == -999.0) { m_perforationData.setLinePositions(100.0, 0.0, 200.0, 0.0); } // 更新UI显示 updateCharts(); updateTableFromFlowSegments(); } void nmWxTimeDependentSkin::onBoundaryMoved(int segmentIndex, double newTime) { // 获取信号发送者 QObject* sender = QObject::sender(); // 根据发送者决定需要更新哪个图表 if(sender == m_pPressureChart && m_pFlowRateChart) { // 压力图发送的信号,更新流量图的外部拖动状态 m_pFlowRateChart->setExternalDragState(true, segmentIndex, newTime); } else if(sender == m_pFlowRateChart && m_pPressureChart) { // 流量图发送的信号,更新压力图的外部拖动状态 m_pPressureChart->setExternalDragState(true, segmentIndex, newTime); } } void nmWxTimeDependentSkin::onBoundaryMovedFinal(int segmentIndex, double newTime) { // 先转成秒并四舍五入 double newSec = toSeconds(newTime, "hr"); bool success = moveFlowSegmentBoundarySec(segmentIndex, newSec); if(success) { if(m_pPressureChart) m_pPressureChart->clearExternalDragState(); if(m_pFlowRateChart) m_pFlowRateChart->clearExternalDragState(); updateCharts(); updateTableFromFlowSegments(); } else { if(m_pPressureChart) m_pPressureChart->clearExternalDragState(); if(m_pFlowRateChart) m_pFlowRateChart->clearExternalDragState(); updateCharts(); } } bool nmWxTimeDependentSkin::insertFlowSegmentAtMiddle(int segmentIndex) { QVector segments = m_perforationData.getFlowSegments(); if(segmentIndex <= 0 || segmentIndex >= segments.size()) { return false; // 第一个段或无效索引,无法在前面插入 } // 获取前一个段的信息 int prevSegmentIndex = segmentIndex - 1; const FlowSegmentData& prevSegment = segments[prevSegmentIndex]; double prevSegmentStartTime = prevSegment.segmentStart.getValue().toDouble(); double prevSegmentEndTime = prevSegment.segmentEnd.getValue().toDouble(); // 检查前一个段是否足够大可以分割(最小间隔0.001小时) if(prevSegmentEndTime - prevSegmentStartTime <= 0.002) { return false; // 前一个段太小,无法分割 } // 计算前一个段的中间时间点 double middleTime = (prevSegmentStartTime + prevSegmentEndTime) / 2.0; // 保存前一个段的原始属性 double originalSkinValue = prevSegment.skinValue.getValue().toDouble(); double originalDSdQValue = prevSegment.dSdQ.getValue().toDouble(); bool wasSelected = prevSegment.isSelected; // 创建第一个流动段(前一个段的前半部分,保留原值) nmDataAttribute newStartTime1("StartTime", prevSegmentStartTime, "hr"); nmDataAttribute newSkinValue1("SkinValue", originalSkinValue, ""); // 保留原来的skin值 nmDataAttribute newSegmentStart1("SegmentStartTime", prevSegmentStartTime, "hr"); nmDataAttribute newSegmentEnd1("SegmentEndTime", middleTime, "hr"); // 创建第二个流动段(前一个段的后半部分,这是新插入的段,值设为0) nmDataAttribute newStartTime2("StartTime", middleTime, "hr"); nmDataAttribute newSkinValue2("SkinValue", 0.0, ""); // 新插入的段默认skin值为0 nmDataAttribute newSegmentStart2("SegmentStartTime", middleTime, "hr"); nmDataAttribute newSegmentEnd2("SegmentEndTime", prevSegmentEndTime, "hr"); FlowSegmentData segment1, segment2; // 根据是否启用流量相关功能来决定如何创建段 if(m_perforationData.isRateDependentEnabled()) { // 启用了流量相关功能,需要处理dSdQ值 nmDataAttribute newDSdQ1("dSdQ", originalDSdQValue, "1/B/D"); // 第一个段保留原来的dSdQ值 nmDataAttribute newDSdQ2("dSdQ", 0.0, "1/B/D"); // 第二个段的dSdQ值为0 segment1 = FlowSegmentData(newStartTime1, newSkinValue1, newSegmentStart1, newSegmentEnd1, newDSdQ1, wasSelected); segment2 = FlowSegmentData(newStartTime2, newSkinValue2, newSegmentStart2, newSegmentEnd2, newDSdQ2, false); // 新段不选中 } else { // 未启用流量相关功能,使用不包含dSdQ的构造函数 segment1 = FlowSegmentData(newStartTime1, newSkinValue1, newSegmentStart1, newSegmentEnd1, wasSelected); segment2 = FlowSegmentData(newStartTime2, newSkinValue2, newSegmentStart2, newSegmentEnd2, false); // 新段不选中 } // 替换前一个段为两个新段 segments.remove(prevSegmentIndex); segments.insert(prevSegmentIndex, segment1); segments.insert(prevSegmentIndex + 1, segment2); // 更新流动段数据 m_perforationData.setFlowSegments(segments); // 保留原选中行,连续插入时始终取该行前一个区间的中点。 selectFlowSegment(segmentIndex + 1); return true; } void nmWxTimeDependentSkin::onSegmentsMerged(int segmentIndex) { bool success = mergeSegmentWithPrevious(segmentIndex); if(success) { if(m_pPressureChart) m_pPressureChart->clearExternalDragState(); if(m_pFlowRateChart) m_pFlowRateChart->clearExternalDragState(); updateCharts(); updateTableFromFlowSegments(); } } void nmWxTimeDependentSkin::onSnapToRateChangesToggled(bool checked) { // 保存状态到数据对象 m_perforationData.setSnapToRateChangesEnabled(checked); // 原来的同步设置 if(m_pPressureChart) m_pPressureChart->setSnapToRateChanges(checked); if(m_pFlowRateChart) m_pFlowRateChart->setSnapToRateChanges(checked); if(m_pChartWidget) m_pChartWidget->setSnapToRateChanges(checked); // 当勾选时,立即把现有所有分界线吸附到阶梯,并自动合并重复 if(checked) { snapAllBoundariesToFlowStepsAndMerge(); } } void nmWxTimeDependentSkin::onFilterClicked() { // 只在图表模式下才能使用filter功能 if(m_pChartWidget == nullptr) { return; } // 如果已经在套索模式,忽略点击(防止重复操作) if(m_pChartWidget->getLassoMode()) { return; } // 进入套索模式(一次性操作) m_pChartWidget->setLassoMode(true); } void nmWxTimeDependentSkin::onResetFilterClicked() { // 只在图表模式下才能使用reset filter功能 if(m_pChartWidget == nullptr) { return; } // 调用图表widget的重置所有×标记点方法 m_pChartWidget->resetAllCrossMarks(); // 保存重置后的位置到数据对象 saveChartPositionsToData(); // 更新编辑框显示 updateEditBoxesFromData(); } void nmWxTimeDependentSkin::onResetLinesClicked() { if(m_pChartWidget != nullptr) { // 调用图表widget的重置方法,让它自己决定重置位置 m_pChartWidget->resetLinesToInitialPositions(); // 保存重置后的位置 saveChartPositionsToData(); // 更新编辑框显示 updateEditBoxesFromData(); } } void nmWxTimeDependentSkin::onAccept() { if(!m_currentWell) { accept(); return; } // 对比模式直接保留当前选项设置,提交前保存拟合线。 if(m_pChartWidget) { saveChartPositionsToData(); } // 副本数据同步回原始射孔段 m_currentWell->updatePerforation(m_currentPerforationIndex, m_perforationData); accept(); } void nmWxTimeDependentSkin::onReject() { reject(); } void nmWxTimeDependentSkin::onWellDestroyed() { // 数据源已失效,只关闭窗口,不再提交副本或处理图表交互。 m_currentWell = nullptr; m_pPressureChart->setCurrentWellData(nullptr); m_pFlowRateChart->setCurrentWellData(nullptr); reject(); } void nmWxTimeDependentSkin::initializeDefaultFlowSegment() { if(m_currentWell == nullptr) { return; } QVector flowPoints = m_currentWell->getFlowPoints(); if(flowPoints.isEmpty()) { return; } // 计算总时间 double totalTime = m_currentWell->getTotalTimeRange(); // 创建一个覆盖整个时间范围的默认流动段,初始状态为选中 QVector segments; nmDataAttribute startTime("StartTime", 0.0, "hr"); nmDataAttribute skinValue("SkinValue", 0.0, ""); nmDataAttribute segmentStart("SegmentStartTime", 0.0, "hr"); nmDataAttribute segmentEnd("SegmentEndTime", totalTime, "hr"); FlowSegmentData defaultSegment; // 根据是否启用流量相关功能来决定如何创建默认段 if(m_perforationData.isRateDependentEnabled()) { nmDataAttribute dSdQ("dSdQ", 0.0, "1/B/D"); defaultSegment = FlowSegmentData(startTime, skinValue, segmentStart, segmentEnd, dSdQ, true); } else { defaultSegment = FlowSegmentData(startTime, skinValue, segmentStart, segmentEnd, true); } segments.append(defaultSegment); m_perforationData.setFlowSegments(segments); m_perforationData.setSelectedSegmentIndex(0); } void nmWxTimeDependentSkin::splitSelectedFlowSegment() { int selectedIndex = m_perforationData.getSelectedSegmentIndex(); QVector segments = m_perforationData.getFlowSegments(); if(selectedIndex < 0 || selectedIndex >= segments.size()) { return; } // 检查选中的流动段是否可以分割 if(!m_currentWell->canFlowSegmentBeSplit(selectedIndex, m_currentPerforationIndex)) { return; } const FlowSegmentData& selectedSegment = segments[selectedIndex]; double segmentStartTime = selectedSegment.segmentStart.getValue().toDouble(); double segmentEndTime = selectedSegment.segmentEnd.getValue().toDouble(); // 获取该段内的流量阶梯 QVector flowSteps = m_currentWell->getFlowStepsInTimeRange(segmentStartTime, segmentEndTime); if(flowSteps.size() <= 1) { return; // 只有一个或没有阶梯段,无法分割 } // 第一个阶梯段的结束时间就是分割点 double firstStepDuration = flowSteps[0].x(); double splitTime = segmentStartTime + firstStepDuration; // 创建第一个流动段(第一个阶梯段) nmDataAttribute newStartTime1("StartTime", segmentStartTime, "hr"); nmDataAttribute newSkinValue1("SkinValue", 0.0, ""); nmDataAttribute newSegmentStart1("SegmentStartTime", segmentStartTime, "hr"); nmDataAttribute newSegmentEnd1("SegmentEndTime", splitTime, "hr"); FlowSegmentData segment1(newStartTime1, newSkinValue1, newSegmentStart1, newSegmentEnd1, true); // 新分离的段自动选中 // 创建第二个流动段(剩余的阶梯段) nmDataAttribute newStartTime2("StartTime", splitTime, "hr"); nmDataAttribute newSkinValue2("SkinValue", 0.0, ""); nmDataAttribute newSegmentStart2("SegmentStartTime", splitTime, "hr"); nmDataAttribute newSegmentEnd2("SegmentEndTime", segmentEndTime, "hr"); FlowSegmentData segment2(newStartTime2, newSkinValue2, newSegmentStart2, newSegmentEnd2, false); // 替换原来的段 segments[selectedIndex] = segment1; segments.insert(selectedIndex + 1, segment2); m_perforationData.setFlowSegments(segments); m_perforationData.setSelectedSegmentIndex(selectedIndex); // 选中新分离出来的第一段 } void nmWxTimeDependentSkin::selectFlowSegment(int segmentIndex) { QVector segments = m_perforationData.getFlowSegments(); if(segmentIndex < 0 || segmentIndex >= segments.size()) { return; } // 取消所有段的选中状态 for(int i = 0; i < segments.size(); ++i) { segments[i].isSelected = (i == segmentIndex); } m_perforationData.setFlowSegments(segments); m_perforationData.setSelectedSegmentIndex(segmentIndex); } void nmWxTimeDependentSkin::splitAllFlowSegments() { QVector flowStepBoundaries = m_currentWell->getAllFlowStepBoundaries(); if(flowStepBoundaries.isEmpty()) return; // 末尾关井段沿用前一段表皮,不单独生成分割点;仍保留总时长作为最后一段终点。 const QVector flowSteps = m_currentWell->getFlowPoints(); for(int i = flowSteps.size() - 1; i > 0 && qFuzzyIsNull(flowSteps[i].y()); --i) { flowStepBoundaries.remove(i); } // 统一量化 for(int i = 0; i < flowStepBoundaries.size(); ++i) { double sec = toSeconds(flowStepBoundaries[i], "hr"); flowStepBoundaries[i] = fromSeconds(sec, "hr"); } qSort(flowStepBoundaries); // 按"秒"去重:把<=1秒的间隔视为重复 const double dedupEpsSec = 1.0; QVector uniq; for(int i = 0; i < flowStepBoundaries.size(); ++i) { if(uniq.isEmpty()) { uniq.push_back(flowStepBoundaries[i]); continue; } double lastSec = toSeconds(uniq.back(), "hr"); double curSec = toSeconds(flowStepBoundaries[i], "hr"); if(qAbs(curSec - lastSec) > dedupEpsSec) uniq.push_back(flowStepBoundaries[i]); } if(uniq.size() < 2) return; QVector newSegments; for(int i = 0; i < uniq.size() - 1; ++i) { double startTime = uniq[i]; double endTime = uniq[i + 1]; // 额外再做一次量化保险 double startSec = toSeconds(startTime, "hr"); double endSec = toSeconds(endTime, "hr"); startTime = fromSeconds(startSec, "hr"); endTime = fromSeconds(endSec, "hr"); // 修改:所有skin值都重置为0,不再保留原值 double resetSkinValue = 0.0; nmDataAttribute newStartTime("StartTime", startTime, "hr"); nmDataAttribute newSkinValue("SkinValue", resetSkinValue, ""); nmDataAttribute newSegmentStart("SegmentStartTime", startTime, "hr"); nmDataAttribute newSegmentEnd("SegmentEndTime", endTime, "hr"); bool selected = (i == 0); FlowSegmentData newSegment; // 根据是否启用流量相关功能来决定如何创建段 if(m_perforationData.isRateDependentEnabled()) { // 启用了流量相关功能,需要创建带有dSdQ的段,并将dSdQ重置为0 nmDataAttribute newDSdQ("dSdQ", 0.0, "1/B/D"); // dSdQ也重置为0 newSegment = FlowSegmentData(newStartTime, newSkinValue, newSegmentStart, newSegmentEnd, newDSdQ, selected); } else { // 未启用流量相关功能,使用不包含dSdQ的构造函数 newSegment = FlowSegmentData(newStartTime, newSkinValue, newSegmentStart, newSegmentEnd, selected); } newSegments.append(newSegment); } for(int i = 0; i < newSegments.size(); ++i) newSegments[i].isSelected = (i == 0); m_perforationData.setFlowSegments(newSegments); m_perforationData.setSelectedSegmentIndex(0); } int nmWxTimeDependentSkin::splitFlowSegmentAtIndex(int segmentIndex) { QVector segments = m_perforationData.getFlowSegments(); if(segmentIndex < 0 || segmentIndex >= segments.size()) { return 0; } // 检查该段是否可以分割 if(!m_currentWell->canFlowSegmentBeSplit(segmentIndex, m_currentPerforationIndex)) { return 0; } const FlowSegmentData& selectedSegment = segments[segmentIndex]; double segmentStartTime = selectedSegment.segmentStart.getValue().toDouble(); // 获取该段内所有流量阶梯的分界点 QVector boundaries = m_currentWell->getFlowStepBoundariesInSegment(segmentIndex, m_currentPerforationIndex); if(boundaries.size() <= 1) { return 0; // 没有分界点或只有一个分界点,无法分割 } // 移除原来的段 segments.remove(segmentIndex); // 根据分界点创建新的流动段 int newSegmentsCount = 0; for(int i = 0; i < boundaries.size(); ++i) { double currentStart = (i == 0) ? segmentStartTime : boundaries[i - 1]; double currentEnd = boundaries[i]; // 创建新的流动段 nmDataAttribute newStartTime("StartTime", currentStart, "hr"); nmDataAttribute newSkinValue("SkinValue", 0.0, ""); nmDataAttribute newSegmentStart("SegmentStartTime", currentStart, "hr"); nmDataAttribute newSegmentEnd("SegmentEndTime", currentEnd, "hr"); // 第一个新段保持选中状态,其他取消选中 bool isSelected = (i == 0 && selectedSegment.isSelected); FlowSegmentData newSegment(newStartTime, newSkinValue, newSegmentStart, newSegmentEnd, isSelected); // 插入到正确的位置 segments.insert(segmentIndex + i, newSegment); newSegmentsCount++; } // 更新流动段数据 m_perforationData.setFlowSegments(segments); return newSegmentsCount - 1; // 返回新增的段数量 } bool nmWxTimeDependentSkin::addFlowSegmentAtTime(double timePoint) { int segmentIndex = m_perforationData.findFlowSegmentByTime(timePoint); if(segmentIndex < 0) return false; // 先找“阶梯结束边界(单位:小时)” double stepBoundaryTime = m_currentWell->findFlowStepBoundaryAtTime(timePoint); if(stepBoundaryTime < 0) return false; // 量化 double stepBoundarySec = toSeconds(stepBoundaryTime, "hr"); stepBoundaryTime = fromSeconds(stepBoundarySec, "hr"); // 再取该段的结束时间做边界校验 const QVector& segments = m_perforationData.getFlowSegments(); double segmentEndTime = segments.at(segmentIndex).segmentEnd.getValue().toDouble(); if(stepBoundaryTime <= segments.at(segmentIndex).segmentStart.getValue().toDouble() || stepBoundaryTime >= segmentEndTime) { return false; } return splitFlowSegmentAtTime(segmentIndex, stepBoundaryTime); } bool nmWxTimeDependentSkin::splitFlowSegmentAtTime(int segmentIndex, double splitTimeHr) { // 统一量化到整秒,再回到小时 double splitSec = toSeconds(splitTimeHr, "hr"); double splitTime = fromSeconds(splitSec, "hr"); QVector segments = m_perforationData.getFlowSegments(); if(segmentIndex < 0 || segmentIndex >= segments.count()) return false; const FlowSegmentData& originalSegment = segments.at(segmentIndex); double segmentStartTime = originalSegment.segmentStart.getValue().toDouble(); double segmentEndTime = originalSegment.segmentEnd.getValue().toDouble(); if(splitTime <= segmentStartTime || splitTime >= segmentEndTime) return false; // 获取原段的skin值和dSdQ值,用于保留给第一个新段 double originalSkinValue = originalSegment.skinValue.getValue().toDouble(); double originalDSdQValue = originalSegment.dSdQ.getValue().toDouble(); // 第一个新段(保留原段的值,选中) nmDataAttribute newStartTime1("StartTime", segmentStartTime, "hr"); nmDataAttribute newSkinValue1("SkinValue", originalSkinValue, ""); // 保留原来的skin值 nmDataAttribute newSegmentStart1("SegmentStartTime", segmentStartTime, "hr"); nmDataAttribute newSegmentEnd1("SegmentEndTime", splitTime, "hr"); // 第二个新段(新增段,值设为0,不选中) nmDataAttribute newStartTime2("StartTime", splitTime, "hr"); nmDataAttribute newSkinValue2("SkinValue", 0.0, ""); // 新增段的skin值为0 nmDataAttribute newSegmentStart2("SegmentStartTime", splitTime, "hr"); nmDataAttribute newSegmentEnd2("SegmentEndTime", segmentEndTime, "hr"); FlowSegmentData segment1, segment2; // 根据是否启用流量相关功能来决定如何创建段 if(m_perforationData.isRateDependentEnabled()) { // 启用了流量相关功能,需要处理dSdQ值 nmDataAttribute newDSdQ1("dSdQ", originalDSdQValue, "1/B/D"); // 第一个段保留原来的dSdQ值 nmDataAttribute newDSdQ2("dSdQ", 0.0, "1/B/D"); // 第二个段的dSdQ值为0 segment1 = FlowSegmentData(newStartTime1, newSkinValue1, newSegmentStart1, newSegmentEnd1, newDSdQ1, true); segment2 = FlowSegmentData(newStartTime2, newSkinValue2, newSegmentStart2, newSegmentEnd2, newDSdQ2, false); } else { // 未启用流量相关功能,使用不包含dSdQ的构造函数 segment1 = FlowSegmentData(newStartTime1, newSkinValue1, newSegmentStart1, newSegmentEnd1, true); segment2 = FlowSegmentData(newStartTime2, newSkinValue2, newSegmentStart2, newSegmentEnd2, false); } segments.remove(segmentIndex); segments.insert(segmentIndex, segment1); segments.insert(segmentIndex + 1, segment2); m_perforationData.setFlowSegments(segments); selectFlowSegment(segmentIndex); return true; } bool nmWxTimeDependentSkin::mergeSegmentWithPrevious(int segmentIndex) { QVector segments = m_perforationData.getFlowSegments(); // 检查索引有效性 if(segmentIndex <= 0 || segmentIndex >= segments.size()) { return false; // 第一个段无法与前段合并 } // 获取要合并的段和前一个段 const FlowSegmentData& currentSegment = segments[segmentIndex]; const FlowSegmentData& prevSegment = segments[segmentIndex - 1]; // 创建合并后的新段 double newStartTime = prevSegment.segmentStart.getValue().toDouble(); double newEndTime = currentSegment.segmentEnd.getValue().toDouble(); double prevSkinValue = prevSegment.skinValue.getValue().toDouble(); // 判断选中状态:如果任一段被选中,则合并后的段被选中 bool mergedIsSelected = prevSegment.isSelected || currentSegment.isSelected; // 创建新的流动段属性 nmDataAttribute mergedStartTime("StartTime", newStartTime, "hr"); nmDataAttribute mergedSkinValue("SkinValue", prevSkinValue, ""); nmDataAttribute mergedSegmentStart("SegmentStartTime", newStartTime, "hr"); nmDataAttribute mergedSegmentEnd("SegmentEndTime", newEndTime, "hr"); FlowSegmentData mergedSegment; // 根据是否启用流量相关功能来决定如何处理dSdQ if(m_perforationData.isRateDependentEnabled()) { // 启用了流量相关功能,需要处理dSdQ值 nmDataAttribute mergedDSdQ; if(currentSegment.isSelected && !prevSegment.isSelected) { // 当前段被选中,前一个段未选中 -> 保留左边(前一个段)的dSdQ mergedDSdQ = prevSegment.dSdQ; } else if(prevSegment.isSelected && !currentSegment.isSelected) { // 前一个段被选中,当前段未选中 -> 保留选中段(前一个段)的dSdQ mergedDSdQ = prevSegment.dSdQ; } else if(prevSegment.isSelected && currentSegment.isSelected) { // 两个段都被选中 -> 这可能是拖动合并,优先保留前一个段的dSdQ mergedDSdQ = prevSegment.dSdQ; } else { // 两个段都未选中 -> 默认保留前一个段的dSdQ mergedDSdQ = prevSegment.dSdQ; } // 使用包含dSdQ的构造函数 mergedSegment = FlowSegmentData(mergedStartTime, mergedSkinValue, mergedSegmentStart, mergedSegmentEnd, mergedDSdQ, mergedIsSelected); } else { // 未启用流量相关功能,使用不包含dSdQ的构造函数(dSdQ会使用默认值) mergedSegment = FlowSegmentData(mergedStartTime, mergedSkinValue, mergedSegmentStart, mergedSegmentEnd, mergedIsSelected); } // 移除原来的两个段(从后往前移除,避免索引变化) segments.remove(segmentIndex); // 先移除当前段 segments.remove(segmentIndex - 1); // 再移除前一个段 // 在原前一个段的位置插入合并后的段 segments.insert(segmentIndex - 1, mergedSegment); // 更新流动段数据 m_perforationData.setFlowSegments(segments); // 设置选中索引为合并后的段 if(mergedIsSelected) { m_perforationData.setSelectedSegmentIndex(segmentIndex - 1); } else { // 如果合并后的段没有被选中,选择第一个段(如果存在) if(segments.size() > 0) { selectFlowSegment(0); } } return true; } bool nmWxTimeDependentSkin::insertFlowSegmentBeforeSegment(int segmentIndex) { const QVector& segments = m_perforationData.getFlowSegments(); if(segmentIndex <= 0 || segmentIndex >= segments.size()) return false; // 使用窗口当前副本的区间,避免刚添加的分段尚未保存到井对象而无法插入。 const int prevSegmentIndex = segmentIndex - 1; const double startSec = toSeconds(segments[prevSegmentIndex].segmentStart.getValue().toDouble(), "hr"); const double endSec = qMin(toSeconds(segments[segmentIndex].segmentStart.getValue().toDouble(), "hr"), toSeconds(m_currentWell->getTotalTimeRange(), "hr")); const QVector boundaries = m_currentWell->getAllFlowStepBoundaries(); double insertSec = -1.0; for(int i = 0; i < boundaries.size(); ++i) { const double boundarySec = toSeconds(boundaries[i], "hr"); if(boundarySec > startSec && boundarySec < endSec && boundarySec > insertSec) { insertSec = boundarySec; } } if(insertSec < 0) return false; const bool success = splitFlowSegmentAtTime(prevSegmentIndex, fromSeconds(insertSec, "hr")); // 插入后仍选中原来的时间行,保持图表与表格的当前流动段一致。 if(success) selectFlowSegment(segmentIndex + 1); return success; } void nmWxTimeDependentSkin::addLastFlowSegmentIfMissing(bool snapToRateChanges) { const double totalTime = m_currentWell->getTotalTimeRange(); if(totalTime <= 0) return; QVector segments = m_perforationData.getFlowSegments(); if(segments.isEmpty()) { initializeDefaultFlowSegment(); segments = m_perforationData.getFlowSegments(); } if(segments.isEmpty()) return; double addTime = -1.0; if(snapToRateChanges) { // 对齐时只补最后一个流量变化点,不把曲线最右侧的终止时间当作变化点。 const QVector boundaries = m_currentWell->getAllFlowStepBoundaries(); const double totalSec = toSeconds(totalTime, "hr"); for(int i = 0; i < boundaries.size(); ++i) { const double boundarySec = toSeconds(boundaries[i], "hr"); if(boundarySec > 0 && boundarySec < totalSec) { addTime = qMax(addTime, fromSeconds(boundarySec, "hr")); } } if(addTime < 0) return; for(int i = 0; i < segments.size(); ++i) { if(toSeconds(segments[i].segmentStart.getValue().toDouble(), "hr") == toSeconds(addTime, "hr")) { return; // 已有结束点时不重复添加。 } } } else { // 未对齐时按内部小时从末行递增100,允许连续添加到原始曲线范围之后。 addTime = segments.last().segmentStart.getValue().toDouble() + 100.0; if(addTime >= segments.last().segmentEnd.getValue().toDouble()) { segments.last().segmentEnd.setValue(addTime + 100.0); m_perforationData.setFlowSegments(segments); } } const int segmentIndex = m_perforationData.findFlowSegmentByTime(addTime); if(segmentIndex >= 0 && splitFlowSegmentAtTime(segmentIndex, addTime)) { selectFlowSegment(segmentIndex + 1); } } void nmWxTimeDependentSkin::snapAllBoundariesToFlowStepsAndMerge() { QVector segments = m_perforationData.getFlowSegments(); if(segments.size() <= 1) return; const double totalTimeHr = m_currentWell->getTotalTimeRange(); const double totalTimeSec = toSeconds(totalTimeHr, "hr"); const double epsSec = 1.0; // 以 1 秒为“重合”阈值 // 1) 吸附所有内部分界线(结果量化到秒后再回写) for(int i = 1; i < segments.size(); ++i) { double curBoundaryHr = segments[i].segmentStart.getValue().toDouble(); double snappedHr = m_currentWell->findNearestFlowStepBoundary(curBoundaryHr); // 量化 double snappedSec = toSeconds(snappedHr, "hr"); // 边界保护 double nextStartHr = (i < segments.size() - 1) ? segments[i + 1].segmentStart.getValue().toDouble() : totalTimeHr; double nextStartSec = toSeconds(nextStartHr, "hr"); if(snappedSec > nextStartSec) snappedSec = nextStartSec; if(snappedSec < 0.0) snappedSec = 0.0; if(snappedSec > totalTimeSec) snappedSec = totalTimeSec; double snappedHrQuant = fromSeconds(snappedSec, "hr"); segments[i].segmentStart.setValue(QString::number(snappedHrQuant, 'f', 5)); segments[i].startTime .setValue(QString::number(snappedHrQuant, 'f', 5)); segments[i - 1].segmentEnd.setValue(QString::number(snappedHrQuant, 'f', 5)); } // 2) 清洗与合并:按“秒”判断 0 长度段 int selectedIdx = m_perforationData.getSelectedSegmentIndex(); for(int i = segments.size() - 1; i >= 0; --i) { double stSec = toSeconds(segments[i].segmentStart.getValue().toDouble(), "hr"); double edSec = toSeconds(segments[i].segmentEnd.getValue().toDouble(), "hr"); if(edSec - stSec <= epsSec) { if(i > 0) { bool leftOrSelfSelected = segments[i - 1].isSelected || segments[i].isSelected; segments.remove(i); if(leftOrSelfSelected) selectedIdx = qMax(0, i - 1); else if(selectedIdx > i) selectedIdx -= 1; else if(selectedIdx == i) selectedIdx = qMax(0, i - 1); } else if(segments.size() > 1) { bool rightOrSelfSelected = segments[0].isSelected || segments[1].isSelected; segments.remove(0); // 把右段起点置 0(量化一致) segments[0].segmentStart.setValue(QString::number(0.0, 'f', 5)); segments[0].startTime .setValue(QString::number(0.0, 'f', 5)); if(rightOrSelfSelected) selectedIdx = 0; else selectedIdx = qMin(selectedIdx, segments.size() - 1); } } } if(segments.isEmpty()) { m_perforationData.setFlowSegments(segments); updateCharts(); updateTableFromFlowSegments(); return; } if(selectedIdx < 0 || selectedIdx >= segments.size()) selectedIdx = 0; for(int i = 0; i < segments.size(); ++i) segments[i].isSelected = (i == selectedIdx); m_perforationData.setFlowSegments(segments); m_perforationData.setSelectedSegmentIndex(selectedIdx); if(m_pPressureChart) m_pPressureChart->clearExternalDragState(); if(m_pFlowRateChart) m_pFlowRateChart->clearExternalDragState(); updateCharts(); updateTableFromFlowSegments(); if(m_pChartWidget) m_pChartWidget->regenerateCrossMarkPoints(); } double nmWxTimeDependentSkin::convertTime(double value, const QString& fromUnit, const QString& toUnit) { // 直接使用底层单位组,兼容内部 hr/sec 与配置中的 h/s 写法。 iUnitGroup* group = iUnitHelper::getUnitGroupByUnit("s"); if(group) { QString source = fromUnit == "sec" ? "s" : fromUnit; QString destination = toUnit == "sec" ? "s" : toUnit; if(group->indexOf("h") >= 0) { if(source == "hr") source = "h"; if(destination == "hr") destination = "h"; } double converted = value; int digits = 6; if(group->convert(source, value, destination, converted, digits)) return converted; } // 单位配置未加载时,沿用原有的内部时间换算。 nmDataAttribute attribute; attribute.setAttributeUnitType(UNIT_TYPE_TIME); return attribute.convertValueByUnitType(value, fromUnit, toUnit); } void nmWxTimeDependentSkin::onTimeUnitChanged(const QString& unit) { if(unit == m_timeDisplayUnit) return; m_timeDisplayUnit = unit; // 复用已屏蔽编辑信号的刷新入口,切换显示单位不回写分段数据。 updateTableFromFlowSegments(); } double nmWxTimeDependentSkin::toSeconds(double value, const QString& unit) { double sec = convertTime(value, unit, "sec"); // 四舍五入到整数秒 return static_cast(qRound(sec)); } double nmWxTimeDependentSkin::fromSeconds(double sec, const QString& unit) { // 从内部秒值转换为框架下拉框选中的单位。 return convertTime(sec, "sec", unit); } bool nmWxTimeDependentSkin::moveFlowSegmentBoundarySec(int segmentIndex, double newBoundarySec) { QVector segments = m_perforationData.getFlowSegments(); // 索引合法性 if(segmentIndex <= 0 || segmentIndex >= segments.size()) return false; // 取相邻边界(都转成“秒并四舍五入”后比较) double prevStartHr = (segmentIndex > 1) ? segments[segmentIndex - 1].segmentStart.getValue().toDouble() : 0.0; double nextStartHr = (segmentIndex < segments.size() - 1) ? segments[segmentIndex + 1].segmentStart.getValue().toDouble() : m_currentWell->getTotalTimeRange(); double prevStartSec = toSeconds(prevStartHr, "hr"); double nextStartSec = toSeconds(nextStartHr, "hr"); // 夹逼 if(newBoundarySec <= prevStartSec || newBoundarySec >= nextStartSec) return false; // 写回:内部仍然存“小时”,但用“秒”为准再转换,保证显示不会抖动 double boundaryHr = fromSeconds(newBoundarySec, "hr"); segments[segmentIndex].segmentStart.setValue(QString::number(boundaryHr, 'f', 5)); segments[segmentIndex].startTime .setValue(QString::number(boundaryHr, 'f', 5)); segments[segmentIndex - 1].segmentEnd.setValue(QString::number(boundaryHr, 'f', 5)); m_perforationData.setFlowSegments(segments); return true; } double nmWxTimeDependentSkin::convertDSdQ(double value, const QString& fromUnit, const QString& toUnit) const { // 直接复用流量组反向换算;旧数据 1/B/D 对应底层 stb/d。 if(fromUnit == toUnit) return value; const QString source = fromUnit == "1/B/D" ? "stb/d" : fromUnit.mid(3, fromUnit.size() - 4); const QString destination = toUnit == "1/B/D" ? "stb/d" : toUnit.mid(3, toUnit.size() - 4); iUnitGroup* group = iUnitHelper::getUnitGroupByUnit("stb/d"); double factor = 1.0; int digits = 6; if(group && group->convert(destination, 1.0, source, factor, digits)) return value * factor; return value; } void nmWxTimeDependentSkin::onAddRateDependentToggled(bool checked) { // 记录到数据类(它会在禁用时把所有 dSdQ 复位为 0) m_perforationData.setRateDependentEnabled(checked); // 重建表头/单位行,并刷新数据 updateTableFromFlowSegments(); // 更新保存的状态 m_rateDependentState = checked; } void nmWxTimeDependentSkin::onDSdQUnitChanged(const QString& unit) { if(unit == m_dSdQDisplayUnit) return; m_dSdQDisplayUnit = unit; // 切换单位只刷新显示,避免 cellChanged 把换算结果重复写回数据。 updateTableFromFlowSegments(); } void nmWxTimeDependentSkin::refreshTableStructure() { // 1) 屏蔽表格信号 bool oldTblBlocked = m_pDataTable->blockSignals(true); // 2) 清理旧的ComboBox if(m_pTimeUnitCombo) { m_pTimeUnitCombo->disconnect(this); delete m_pTimeUnitCombo; m_pTimeUnitCombo = nullptr; } if(m_pDSdQUnitCombo) { m_pDSdQUnitCombo->disconnect(this); delete m_pDSdQUnitCombo; m_pDSdQUnitCombo = nullptr; } // 3) 重建表头/列 m_pDataTable->clear(); const bool enableRateDep = m_perforationData.isRateDependentEnabled(); const int colCount = enableRateDep ? 4 : 3; m_pDataTable->setColumnCount(colCount); QStringList headers; headers << tr("") << tr("Start time") << tr("Skin"); if(enableRateDep) headers << tr("dS/dQ"); m_pDataTable->setHorizontalHeaderLabels(headers); QHeaderView* header = m_pDataTable->horizontalHeader(); header->setResizeMode(0, QHeaderView::Fixed); header->setResizeMode(1, QHeaderView::Stretch); header->setResizeMode(2, QHeaderView::Stretch); if(enableRateDep) header->setResizeMode(3, QHeaderView::Stretch); m_pDataTable->setColumnWidth(0, 20); // 4) 创建单位行和ComboBox if(m_pDataTable->rowCount() == 0) { m_pDataTable->insertRow(0); m_pDataTable->setRowHeight(0, 24); } createTimeUnitCombo(); // Skin列占位 QTableWidgetItem* emptySkin = new QTableWidgetItem(""); emptySkin->setFlags(Qt::ItemIsEnabled); m_pDataTable->setItem(0, 2, emptySkin); if(enableRateDep) { createDSdQUnitCombo(); } // 5) 恢复表格信号 m_pDataTable->blockSignals(oldTblBlocked); } void nmWxTimeDependentSkin::onShowDatesToggled(bool checked) { // 更新数据状态 m_perforationData.setShowDatesEnabled(checked); // 如果启用日期显示,设置默认开始时间 if(checked && m_perforationData.getStartDateTime().isNull()) { m_perforationData.setStartDateTime(QDateTime::currentDateTime()); } // 更新时间单位下拉框的可见性 updateTimeUnitComboVisibility(); // 刷新表格显示 updateTableFromFlowSegments(); } void nmWxTimeDependentSkin::updateTimeUnitComboVisibility() { bool showDates = m_perforationData.isShowDatesEnabled(); if(showDates) { // 在使用setCellWidget替换控件前,先将指针置空 if(m_pTimeUnitCombo) { m_pTimeUnitCombo->disconnect(this); m_pTimeUnitCombo = nullptr; // 关键:置空指针 } // 创建并设置标签(setCellWidget会自动删除旧控件) QLabel* dateTimeLabel = new QLabel(tr("Date Time"), m_pDataTable); dateTimeLabel->setAlignment(Qt::AlignCenter); dateTimeLabel->setStyleSheet("QLabel { color: #333; font-size: 12px; }"); m_pDataTable->setCellWidget(0, 1, dateTimeLabel); } else { // 恢复ComboBox显示 if(!m_pTimeUnitCombo) { createTimeUnitCombo(); } else { m_pTimeUnitCombo->setVisible(true); } } } QString nmWxTimeDependentSkin::formatTimeDisplay(double timeInHours, int segmentIndex) const { bool showDates = m_perforationData.isShowDatesEnabled(); if(showDates) { // 显示绝对时间 QDateTime startDateTime = m_perforationData.getStartDateTime(); QDateTime segmentDateTime = startDateTime.addSecs(static_cast(timeInHours * 3600)); return segmentDateTime.toString("yyyy-MM-dd hh:mm:ss"); } else { // 显示相对时间 double startSec = toSeconds(timeInHours, "hr"); double startDisp = fromSeconds(startSec, m_timeDisplayUnit); return QString::number(startDisp, 'f', 5); } }