From 2be80141cd0356f8c345f9920b31838257bd6002 Mon Sep 17 00:00:00 2001 From: lh <2334563547@qq.com> Date: Mon, 7 Sep 2026 14:26:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(nmNum):=20=E6=94=AF=E6=8C=81=E5=8C=85?= =?UTF-8?q?=E5=90=AB=E4=BA=95=E9=85=8D=E7=BD=AE=E5=8E=8B=E5=8A=9B=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E4=B8=8E=E5=88=86=E7=9B=B8=E4=BA=A7=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在包含井对话框中增加井别、压力、分相产量和流动段选择 - 使用 Gauge Code 关联并保存各井压力流量记录 - 支持独立启用油、气、水相产量并同步求解输入 - 完善记录切换、结果失效、中文翻译及界面提示 --- Bin/Config/Lang/cn/nmNum_cn.ts | 25 +- .../nmCalculation/nmCalculationPebiGrid.h | 2 + Include/nmNum/nmData/nmDataWellBase.h | 23 +- .../nmNum/nmSubWxs/nmWellPressureFlowPage.h | 10 +- .../nmNum/nmSubWxs/nmWxIncludeOtherWells.h | 71 +- .../nmCalculationDllPebiSolverTask.cpp | 12 +- .../nmCalculation/nmCalculationPebiGrid.cpp | 53 +- Src/nmNum/nmData/nmDataAnalyzeManager.cpp | 13 +- Src/nmNum/nmData/nmDataWellBase.cpp | 158 +++- Src/nmNum/nmSubWxs/nmWellPressureFlowPage.cpp | 86 +- Src/nmNum/nmSubWxs/nmWxIncludeOtherWells.cpp | 762 ++++++++++++++---- Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp | 108 ++- 12 files changed, 1010 insertions(+), 313 deletions(-) diff --git a/Bin/Config/Lang/cn/nmNum_cn.ts b/Bin/Config/Lang/cn/nmNum_cn.ts index 91a9ef4..da3d8c2 100644 --- a/Bin/Config/Lang/cn/nmNum_cn.ts +++ b/Bin/Config/Lang/cn/nmNum_cn.ts @@ -5189,26 +5189,44 @@ Please check your input coordinates. Name 井名 + + Well category + 井别 + Included 是否包含 Oil production - 产油量 + 油产量 Gas production - 产气量 + 气产量 Water production - 产水量 + 水产量 Pressure 压力 + Flow segment流动段 + Oil well油井 + Gas well气井 + Water well水井 + Unknown未知 + None + (unnamed)(未命名) + (Empty)(空) + (Corrupt)(损坏) + (Invalid)(无效) + (No data)(无数据) + Segment %1第 %1 段 + No eligible wells没有可配置的其他井 + Select at least one production record first.请先选择至少一个产量相。 OK 确定 @@ -7192,6 +7210,7 @@ Average pressure in contour: %2 MPa Pressure Record:压力记录: Flow Record:流量记录: Display Phase:显示相: + This page only displays the selected record.此页面仅展示当前选中的记录。 (Invalid) (无效) (Empty) (空) (Corrupt) (损坏) diff --git a/Include/nmNum/nmCalculation/nmCalculationPebiGrid.h b/Include/nmNum/nmCalculation/nmCalculationPebiGrid.h index 4f2b7a2..88939f8 100644 --- a/Include/nmNum/nmCalculation/nmCalculationPebiGrid.h +++ b/Include/nmNum/nmCalculation/nmCalculationPebiGrid.h @@ -28,6 +28,7 @@ struct nmPebiWellInputSnapshot { nmPebiWellInputSnapshot() : m_eWellCategory(NM_WellCategory_Unknown), + m_eFlowSchedulePhase(PHASE_UNKNOWN), m_nFlowSectionIndex(1), m_dRadius(0.0), m_dWellboreStorage(0.0), @@ -49,6 +50,7 @@ struct nmPebiWellInputSnapshot QVector m_vecGasRates; ///< 与公共流动段逐项对应的气相流量。 QVector m_vecWaterRates; ///< 与公共流动段逐项对应的水相流量。 NM_WELL_CATEGORY m_eWellCategory; ///< 求解时冻结的油井、气井或水井井别。 + NM_PHASE_TYPE m_eFlowSchedulePhase; ///< 旧单相后处理使用的当前制度参考相。 /** @brief 求解时冻结的历史压力曲线。 */ QVector > m_vecHistoryPressure; /** @brief 求解时冻结的历史双对数曲线。 */ diff --git a/Include/nmNum/nmData/nmDataWellBase.h b/Include/nmNum/nmData/nmDataWellBase.h index 797b98e..88d3e74 100644 --- a/Include/nmNum/nmData/nmDataWellBase.h +++ b/Include/nmNum/nmData/nmDataWellBase.h @@ -153,9 +153,13 @@ public: bool hasFlowPhaseData(NM_PHASE_TYPE eFlowPhase) const; /** @brief 油、气、水中是否至少有一相包含真实流动段。 */ bool hasAnyFlowData() const; - /** @brief 返回井别对应相去除可选 (0,0) 占位点后的真实流动段。 */ + /** @brief 全部框架记录中是否存在至少一个可用的非零产量相。 */ + bool hasAvailableFlowRate() const; + /** @brief 返回当前启用相的公共流量制度;优先取井别对应相。 */ QVector getFlowSegmentPoints() const; - /** @brief 返回井别对应相的真实流动段数量。 */ + /** @brief 返回当前公共流量制度采用的参考相;没有启用相时返回 Unknown。 */ + NM_PHASE_TYPE getFlowSchedulePhase() const; + /** @brief 返回当前启用相共用的真实流动段数量。 */ int getFlowSegmentCount() const; /** @brief 校验当前流动段索引是否位于一基的真实流动段范围内。 */ bool hasValidFlowSectionIndex() const; @@ -183,6 +187,18 @@ public: QVector getPressureRecords() const; /** @brief 返回全部流量记录(包含损坏/空/身份无效记录)。 */ QVector getFlowRecords() const; + /** @brief 返回指定相是否作为当前流量制度的一部分传给求解器。 */ + bool isFlowPhaseUsed(NM_PHASE_TYPE eFlowPhase) const; + /** @brief 设置指定相是否参与当前流量制度;井别不会随此状态改变。 */ + void setFlowPhaseUsed(NM_PHASE_TYPE eFlowPhase, bool bUsed); + // 以下显式接口供现有 UI 和快照组装代码使用,语义与上面的分相接口一致。 + // 三个开关只控制求解器输入,不修改框架原始记录,也不改变工程井井别。 + bool getUseOilRate() const; + bool getUseGasRate() const; + bool getUseWaterRate() const; + void setUseOilRate(bool bUsed); + void setUseGasRate(bool bUsed); + void setUseWaterRate(bool bUsed); int getIndexF() const; void setIndexF(const int newIndex); @@ -319,6 +335,9 @@ protected: QVector m_vecFlowRecords; // 全部流量测量记录(可能包含损坏/空/身份无效记录) QString m_sSelectedPressureGaugeCode; // 当前选中的压力记录 Gauge Code,为空表示未选中 QString m_sSelectedFlowGaugeCode; // 当前选中的流量记录 Gauge Code,为空表示未选中 + bool m_bUseOilRate; // 当前流量制度是否传入油相产量 + bool m_bUseGasRate; // 当前流量制度是否传入气相产量 + bool m_bUseWaterRate; // 当前流量制度是否传入水相产量 int m_nIndexF; // 当前流量段的索引 diff --git a/Include/nmNum/nmSubWxs/nmWellPressureFlowPage.h b/Include/nmNum/nmSubWxs/nmWellPressureFlowPage.h index 6bb9304..65097ad 100644 --- a/Include/nmNum/nmSubWxs/nmWellPressureFlowPage.h +++ b/Include/nmNum/nmSubWxs/nmWellPressureFlowPage.h @@ -59,12 +59,6 @@ private slots: /** @brief 将流量曲线点击时间映射为表格行选择。 */ void slotFlowChartPointClicked(double dTimePoint); - /** @brief 切换压力记录下拉框,按新选中的 Gauge Code 重新读取数据。 */ - void slotPressureGaugeChanged(int nIndex); - - /** @brief 切换流量记录下拉框,按新选中的 Gauge Code 恢复流动段索引并重新读取数据。 */ - void slotFlowGaugeChanged(int nIndex); - /** @brief 切换显示相下拉框,仅重新按新相取点刷新视图,不改变井数据。 */ void slotDisplayPhaseChanged(int nIndex); @@ -168,8 +162,8 @@ private: int m_nSelectedFlowSegment; QWidget* m_pRecordSelectionBar; ///< 顶部压力/流量记录与显示相下拉框所在行容器。 - QComboBox* m_pPressureGaugeCombo; ///< 压力记录下拉框,itemData 存 Gauge Code。 - QComboBox* m_pFlowGaugeCombo; ///< 流量记录下拉框,itemData 存 Gauge Code。 + QComboBox* m_pPressureGaugeCombo; ///< 当前压力记录只读展示,itemData 存 Gauge Code。 + QComboBox* m_pFlowGaugeCombo; ///< 当前流量记录只读展示,itemData 存 Gauge Code。 QComboBox* m_pDisplayPhaseCombo; ///< 显示相下拉框,itemData 存 NM_PHASE_TYPE。 QWidget* m_pDisplayPhaseContainer; ///< 承载显示相下拉框与标签,整体隐藏用。 QPushButton* m_pCheckDataButton; ///< 临时“检查数据”诊断按钮,只读展示用。 diff --git a/Include/nmNum/nmSubWxs/nmWxIncludeOtherWells.h b/Include/nmNum/nmSubWxs/nmWxIncludeOtherWells.h index c064b08..fde7fe2 100644 --- a/Include/nmNum/nmSubWxs/nmWxIncludeOtherWells.h +++ b/Include/nmNum/nmSubWxs/nmWxIncludeOtherWells.h @@ -4,8 +4,10 @@ #include "nmSubWxs_global.h" #include "iDlgBase.h" #include "nmDataNumericalAnalysisCase.h" +#include "nmDataGaugeRecord.h" #include #include +#include #include #include #include @@ -25,10 +27,14 @@ struct WellDataRow { bool m_bCanCalculate; ///< 当前井型和几何参数是否可进入 PEBI 模型。 NM_WELL_CATEGORY m_eWellCategory; ///< 工程井井别,用于单相记录归相及求解模型匹配。 NM_CASE_WELL_MODE m_eMode; ///< 本列表固定保存有产量主动井角色。 - QString m_sOilProd; ///< 油产量列的摘要文本。 - QString m_sGasProd; ///< 气产量列的摘要文本。 - QString m_sWaterProd; ///< 水产量列的摘要文本。 - QString m_sPressure; ///< 压力历史列的摘要文本。 + QString m_sPressureGaugeCode; ///< 本次选择的压力记录,为空表示“无”。 + QString m_sFlowGaugeCode; ///< 三个产量列共同使用的流量记录。 + bool m_bUseOilRate; ///< 是否使用公共流量记录中的油相产量。 + bool m_bUseGasRate; ///< 是否使用公共流量记录中的气相产量。 + bool m_bUseWaterRate; ///< 是否使用公共流量记录中的水相产量。 + int m_nFlowSegmentIndex; ///< 当前流量记录的一基流动段索引。 + QVector m_vecPressureRecords; ///< 供压力列建立下拉项。 + QVector m_vecFlowRecords; ///< 供三个产量列建立下拉项。 /** @brief 初始化未勾选、不可计算的主动井候选行。 */ WellDataRow() @@ -36,13 +42,20 @@ struct WellDataRow { m_bIsPrimary(false), m_bCanCalculate(false), m_eWellCategory(NM_WellCategory_Unknown), - m_eMode(NM_CaseWell_RateControlled) + m_eMode(NM_CaseWell_RateControlled), + m_bUseOilRate(false), + m_bUseGasRate(false), + m_bUseWaterRate(false), + m_nFlowSegmentIndex(0) { } - /** @brief WellCode 有效且井型/几何可计算时允许用户勾选。 */ + /** @brief 至少选择一个产量相时,该井才可作为主动井提交。 */ bool canCalculate() const { - return m_bCanCalculate && !m_sWellCode.isEmpty(); + return m_bCanCalculate && !m_sWellCode.isEmpty() && + !m_sFlowGaugeCode.isEmpty() && + (m_bUseOilRate || m_bUseGasRate || m_bUseWaterRate) && + m_nFlowSegmentIndex > 0; } }; @@ -72,15 +85,31 @@ private: /** @brief 将一条候选井数据写入指定表格行。 */ void addWellDataToTable(const WellDataRow& data, int row); + /** @brief 根据一行的公共流量记录重新建立三个分相下拉框和流动段。 */ + void refreshRowEditors(int row); + /** @brief 校正一行的分相状态及一基流动段索引。 */ + void normalizeFlowSelection(WellDataRow& data) const; + /** @brief 更新包含复选框;没有有效分相选择时自动取消包含。 */ + void updateIncludeCheckBox(int row); + /** @brief 填充一列分相流量记录选项。 */ + void populateRateCombo(QComboBox* pComboBox, + const WellDataRow& data, NM_PHASE_TYPE ePhase) const; + /** @brief 填充压力记录选项。 */ + void populatePressureCombo(QComboBox* pComboBox, + const WellDataRow& data) const; + /** @brief 填充当前公共流量记录对应的流动段选项。 */ + void populateFlowSegmentCombo(QComboBox* pComboBox, + const WellDataRow& data) const; - // 更新复选框状态 - //void updateCheckBoxState(int row); - - private slots: - /** @brief 响应整行点击,并切换该行包含状态。 */ - void onItemClicked(QTableWidgetItem* item); - /** @brief 把复选框状态同步回对应的 WellDataRow。 */ - void onCheckBoxStateChanged(int state); +private slots: + /** @brief 把复选框状态同步回对应的 WellDataRow。 */ + void onCheckBoxStateChanged(int state); + /** @brief 切换某一相使用的流量记录;其他已选相同步到同一记录。 */ + void onRateSourceChanged(int nIndex); + /** @brief 切换该井使用的压力记录。 */ + void onPressureSourceChanged(int nIndex); + /** @brief 切换该井的一基流动段索引。 */ + void onFlowSegmentChanged(int nIndex); private: /** @brief 当前对话框所属的数据管理器,不拥有对象。 */ @@ -92,6 +121,18 @@ private: QVBoxLayout* m_pMainLayout; ///< 对话框主布局。 QVector m_vecWellData; ///< 与表格行一一对应的候选井数据。 + + enum TableColumn { + ColumnName = 0, + ColumnWellCategory, + ColumnIncluded, + ColumnOilRate, + ColumnGasRate, + ColumnWaterRate, + ColumnPressure, + ColumnFlowSegment, + ColumnCount + }; }; #endif // NMWXINCLUDEOTHERWELLS_H diff --git a/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp b/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp index 4b6b5c8..74b22de 100644 --- a/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp @@ -176,17 +176,17 @@ bool isFiniteSolverNumber(double value) #endif } -// 旧的双对数曲线 DLL 仍是单相接口,只在结果后处理时按井别取参考相。 -// PEBI 主求解已经在场景中接收油、气、水三相全部流量。 +// 旧的双对数曲线 DLL 仍是单相接口,只在结果后处理时取输入捕获阶段 +// 冻结的制度参考相。PEBI 主求解仍在场景中接收油、气、水三相全部流量。 const QVector* referenceRatesOf( const nmPebiWellInputSnapshot& oWellInput) { - switch(oWellInput.m_eWellCategory) { - case NM_WellCategory_Oil: + switch(oWellInput.m_eFlowSchedulePhase) { + case PHASE_Oil: return &oWellInput.m_vecOilRates; - case NM_WellCategory_Gas: + case PHASE_Gas: return &oWellInput.m_vecGasRates; - case NM_WellCategory_Water: + case PHASE_Water: return &oWellInput.m_vecWaterRates; default: return nullptr; diff --git a/Src/nmNum/nmCalculation/nmCalculationPebiGrid.cpp b/Src/nmNum/nmCalculation/nmCalculationPebiGrid.cpp index 7f2d963..c3baa86 100644 --- a/Src/nmNum/nmCalculation/nmCalculationPebiGrid.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationPebiGrid.cpp @@ -227,33 +227,25 @@ bool captureWellFlowSchedule( const QVector vecWaterPoints = pWellData->getFlowSegmentPoints(PHASE_Water); - const QVector* pTimePoints = nullptr; - switch(pWellData->getReferenceFlowPhase()) { - case PHASE_Oil: - pTimePoints = &vecOilPoints; - break; - case PHASE_Gas: - pTimePoints = &vecGasPoints; - break; - case PHASE_Water: - pTimePoints = &vecWaterPoints; - break; - default: - break; - } - if(pTimePoints == nullptr || pTimePoints->isEmpty()) { - pTimePoints = !vecOilPoints.isEmpty() ? &vecOilPoints - : (!vecGasPoints.isEmpty() ? &vecGasPoints - : (!vecWaterPoints.isEmpty() ? &vecWaterPoints : nullptr)); - } - if(pTimePoints == nullptr || pTimePoints->isEmpty()) { + // 公共段时长取自当前至少一个启用相;井别只影响首选顺序,不裁剪相数据。 + oWellInput.m_eFlowSchedulePhase = + pWellData->getFlowSchedulePhase(); + const QVector vecTimePoints = + pWellData->getFlowSegmentPoints(); + if(oWellInput.m_eFlowSchedulePhase == PHASE_UNKNOWN || + vecTimePoints.isEmpty()) { return false; } - const int nSegmentCount = pTimePoints->size(); - if((!vecOilPoints.isEmpty() && vecOilPoints.size() != nSegmentCount) || - (!vecGasPoints.isEmpty() && vecGasPoints.size() != nSegmentCount) || - (!vecWaterPoints.isEmpty() && vecWaterPoints.size() != nSegmentCount)) { + const int nSegmentCount = vecTimePoints.size(); + // GaugeDataEx2 本身只有一列公共段时长。这里仅防御启用相数组长度异常, + // 未启用相即使保留原始数据,也不会参与本次快照组装。 + if((pWellData->getUseOilRate() && !vecOilPoints.isEmpty() && + vecOilPoints.size() != nSegmentCount) || + (pWellData->getUseGasRate() && !vecGasPoints.isEmpty() && + vecGasPoints.size() != nSegmentCount) || + (pWellData->getUseWaterRate() && !vecWaterPoints.isEmpty() && + vecWaterPoints.size() != nSegmentCount)) { return false; } @@ -261,14 +253,19 @@ bool captureWellFlowSchedule( oWellInput.m_vecOilRates.reserve(nSegmentCount); oWellInput.m_vecGasRates.reserve(nSegmentCount); oWellInput.m_vecWaterRates.reserve(nSegmentCount); + // 求解器要求 qo/qg/qw 与 Rate.t 等长。用户设为“无”的相用同轴零值占位, + // 只关闭该相源汇项,不改变其他相的段数和时间位置。 for(int nIndex = 0; nIndex < nSegmentCount; ++nIndex) { - oWellInput.m_vecFlowDurations.append((*pTimePoints)[nIndex].x()); + oWellInput.m_vecFlowDurations.append(vecTimePoints[nIndex].x()); oWellInput.m_vecOilRates.append( - vecOilPoints.isEmpty() ? 0.0 : vecOilPoints[nIndex].y()); + !pWellData->getUseOilRate() || vecOilPoints.isEmpty() + ? 0.0 : vecOilPoints[nIndex].y()); oWellInput.m_vecGasRates.append( - vecGasPoints.isEmpty() ? 0.0 : vecGasPoints[nIndex].y()); + !pWellData->getUseGasRate() || vecGasPoints.isEmpty() + ? 0.0 : vecGasPoints[nIndex].y()); oWellInput.m_vecWaterRates.append( - vecWaterPoints.isEmpty() ? 0.0 : vecWaterPoints[nIndex].y()); + !pWellData->getUseWaterRate() || vecWaterPoints.isEmpty() + ? 0.0 : vecWaterPoints[nIndex].y()); } return true; } diff --git a/Src/nmNum/nmData/nmDataAnalyzeManager.cpp b/Src/nmNum/nmData/nmDataAnalyzeManager.cpp index 798d8c5..72fa9d0 100644 --- a/Src/nmNum/nmData/nmDataAnalyzeManager.cpp +++ b/Src/nmNum/nmData/nmDataAnalyzeManager.cpp @@ -4150,16 +4150,17 @@ nmDataAnalyzeManager::getEffectiveCalculationWells() const continue; } - const bool bHasValidRate = - nmIsValidWellCategory(pWellData->getWellCategory()) && - pWellData->getFlowSegmentCount() > 0; - if(!bHasValidRate) { + const bool bHasAvailableRate = + nmIsValidWellCategory(pWellData->getWellCategory()) && + pWellData->hasAvailableFlowRate(); + if(!bHasAvailableRate) { vecEffectiveWells.append(nmCalculationWellRef( pWellData->getWellCode(), NM_CaseWell_Observation)); setEffectiveWellCodes.insert(pWellData->getWellCode()); - } else if(setIncludedRateWellCodes.contains( - pWellData->getWellCode())) { + } else if(setIncludedRateWellCodes.contains( + pWellData->getWellCode()) && + pWellData->getFlowSegmentCount() > 0) { vecEffectiveWells.append(nmCalculationWellRef( pWellData->getWellCode(), NM_CaseWell_RateControlled)); diff --git a/Src/nmNum/nmData/nmDataWellBase.cpp b/Src/nmNum/nmData/nmDataWellBase.cpp index c7f1c21..fe5451e 100644 --- a/Src/nmNum/nmData/nmDataWellBase.cpp +++ b/Src/nmNum/nmData/nmDataWellBase.cpp @@ -48,8 +48,22 @@ static const TRecord* findGaugeRecordByCode(const QVector& vecRecords, static const QString kManualPressureGaugeCode = QString::fromLatin1("__ManualPressureRecord__"); static const QString kManualFlowGaugeCode = QString::fromLatin1("__ManualFlowRecord__"); +// 判断一组流动段中是否存在实际产量;公共 (0,0) 占位点和全零相均不算产量。 +static bool hasNonZeroFlowSegment(const QVector& vecPoints) +{ + for(int nIndex = 0; nIndex < vecPoints.size(); ++nIndex) { + if(vecPoints[nIndex].y() != 0.0) { + return true; + } + } + return false; +} + nmDataWellBase::nmDataWellBase() : m_eWellCategory(NM_WellCategory_Unknown) + , m_bUseOilRate(true) + , m_bUseGasRate(true) + , m_bUseWaterRate(true) , m_nIndexF(0) , m_bPlotVisible(true) , m_bTimeDependentSkin(false) @@ -165,6 +179,9 @@ nmDataWellBase& nmDataWellBase::operator=(const nmDataWellBase& other) m_vecFlowRecords = other.m_vecFlowRecords; m_sSelectedPressureGaugeCode = other.m_sSelectedPressureGaugeCode; m_sSelectedFlowGaugeCode = other.m_sSelectedFlowGaugeCode; + m_bUseOilRate = other.m_bUseOilRate; + m_bUseGasRate = other.m_bUseGasRate; + m_bUseWaterRate = other.m_bUseWaterRate; m_nIndexF = other.m_nIndexF; m_finalWellboreStorage = other.m_finalWellboreStorage; m_inputWellHead = other.m_inputWellHead; @@ -285,6 +302,10 @@ rapidjson::Value nmDataWellBase::ToJsonValue(rapidjson::Document::AllocatorType& rapidjson::Value(baFlowGaugeCode.constData(), static_cast(baFlowGaugeCode.size()), allocator).Move(), allocator); + // 井别只描述工程井属性,三相是否参与求解必须独立保存。 + wellObject.AddMember("UseOilRate", m_bUseOilRate, allocator); + wellObject.AddMember("UseGasRate", m_bUseGasRate, allocator); + wellObject.AddMember("UseWaterRate", m_bUseWaterRate, allocator); // 序列化射孔段集合 (现在处理指针) rapidjson::Value perforationsArray(rapidjson::kArrayType); @@ -455,6 +476,18 @@ void nmDataWellBase::FromJsonValue(const rapidjson::Value& jsonValue) m_sSelectedFlowGaugeCode = QString::fromUtf8( jsonValue["SelectedFlowGaugeCode"].GetString()); } + if(jsonValue.HasMember("UseOilRate") && + jsonValue["UseOilRate"].IsBool()) { + m_bUseOilRate = jsonValue["UseOilRate"].GetBool(); + } + if(jsonValue.HasMember("UseGasRate") && + jsonValue["UseGasRate"].IsBool()) { + m_bUseGasRate = jsonValue["UseGasRate"].GetBool(); + } + if(jsonValue.HasMember("UseWaterRate") && + jsonValue["UseWaterRate"].IsBool()) { + m_bUseWaterRate = jsonValue["UseWaterRate"].GetBool(); + } // 反序列化射孔段集合 (现在处理指针) if(jsonValue.HasMember("Perforations") && jsonValue["Perforations"].IsArray()) { @@ -909,6 +942,72 @@ QVector nmDataWellBase::getFlowRecords() const return m_vecFlowRecords; } +bool nmDataWellBase::isFlowPhaseUsed(NM_PHASE_TYPE eFlowPhase) const +{ + // 分相开关是数值模块自己的求解配置,不反向写入框架 GaugeDataEx2。 + switch(eFlowPhase) { + case PHASE_Oil: + return m_bUseOilRate; + case PHASE_Gas: + return m_bUseGasRate; + case PHASE_Water: + return m_bUseWaterRate; + default: + return false; + } +} + +void nmDataWellBase::setFlowPhaseUsed( + NM_PHASE_TYPE eFlowPhase, bool bUsed) +{ + // 未知相和组合相不是独立的求解数组,不能映射到任一分相开关。 + switch(eFlowPhase) { + case PHASE_Oil: + m_bUseOilRate = bUsed; + break; + case PHASE_Gas: + m_bUseGasRate = bUsed; + break; + case PHASE_Water: + m_bUseWaterRate = bUsed; + break; + default: + break; + } +} + +bool nmDataWellBase::getUseOilRate() const +{ + // 保留显式 getter,便于 Qt 4.8 时代的调用代码直接读取三个固定相。 + return m_bUseOilRate; +} + +bool nmDataWellBase::getUseGasRate() const +{ + return m_bUseGasRate; +} + +bool nmDataWellBase::getUseWaterRate() const +{ + return m_bUseWaterRate; +} + +void nmDataWellBase::setUseOilRate(bool bUsed) +{ + // 这里只保存选择状态;记录存在性和流动段合法性由提交及快照阶段统一处理。 + m_bUseOilRate = bUsed; +} + +void nmDataWellBase::setUseGasRate(bool bUsed) +{ + m_bUseGasRate = bUsed; +} + +void nmDataWellBase::setUseWaterRate(bool bUsed) +{ + m_bUseWaterRate = bUsed; +} + QVector nmDataWellBase::getFlowSegmentPoints( NM_PHASE_TYPE eFlowPhase) const { @@ -958,14 +1057,61 @@ bool nmDataWellBase::hasAnyFlowData() const hasFlowPhaseData(PHASE_Water); } +bool nmDataWellBase::hasAvailableFlowRate() const +{ + // 这里判断框架事实,不受当前 Gauge Code 和三个分相开关影响。 + // 数据管理器用它区分“未包含的产量井”和“自动观察井”。 + for(int nRecordIndex = 0; + nRecordIndex < m_vecFlowRecords.size(); + ++nRecordIndex) { + const nmFlowGaugeRecord& oRecord = m_vecFlowRecords[nRecordIndex]; + if(oRecord.eStatus != NM_GaugeRecord_Usable) { + continue; + } + if(hasNonZeroFlowSegment(oRecord.vecOilPoints) || + hasNonZeroFlowSegment(oRecord.vecGasPoints) || + hasNonZeroFlowSegment(oRecord.vecWaterPoints)) { + return true; + } + } + return false; +} + QVector nmDataWellBase::getFlowSegmentPoints() const { - return getFlowSegmentPoints(getReferenceFlowPhase()); + const NM_PHASE_TYPE eSchedulePhase = getFlowSchedulePhase(); + return eSchedulePhase == PHASE_UNKNOWN + ? QVector() : getFlowSegmentPoints(eSchedulePhase); +} + +NM_PHASE_TYPE nmDataWellBase::getFlowSchedulePhase() const +{ + // 公共流量制度优先使用井别对应且已启用的相;该相被设为“无”时, + // 再按油、气、水顺序选择其他已启用且有数据的相作为参考流量。 + const NM_PHASE_TYPE eReferencePhase = getReferenceFlowPhase(); + if(isFlowPhaseUsed(eReferencePhase) && hasNonZeroFlowSegment( + getFlowSegmentPoints(eReferencePhase))) { + return eReferencePhase; + } + const NM_PHASE_TYPE arrPhases[] = { + PHASE_Oil, PHASE_Gas, PHASE_Water + }; + for(int nIndex = 0; nIndex < 3; ++nIndex) { + if(!isFlowPhaseUsed(arrPhases[nIndex])) { + continue; + } + const QVector vecPoints = + getFlowSegmentPoints(arrPhases[nIndex]); + if(hasNonZeroFlowSegment(vecPoints)) { + return arrPhases[nIndex]; + } + } + return PHASE_UNKNOWN; } int nmDataWellBase::getFlowSegmentCount() const { - return getFlowSegmentCount(getReferenceFlowPhase()); + return getFlowSegmentPoints().size(); } bool nmDataWellBase::hasValidFlowSectionIndex() const @@ -1762,10 +1908,10 @@ bool nmDataWellBase::exportHistoryRateToCsv(const QString& outDir) QTextStream out(&file); - // 4) 表头与井别保持一致,便于离线排查实际进入求解器的相别。 - const char* pRateColumn = m_eWellCategory == NM_WellCategory_Gas - ? "qg" : (m_eWellCategory == NM_WellCategory_Water - ? "qw" : "qo"); + // 4) 表头跟随当前制度参考相,避免井别相被设为“无”后误标流量列。 + const NM_PHASE_TYPE eSchedulePhase = getFlowSchedulePhase(); + const char* pRateColumn = eSchedulePhase == PHASE_Gas + ? "qg" : (eSchedulePhase == PHASE_Water ? "qw" : "qo"); out << "t," << pRateColumn << ",sectionIndex\n"; // 5) 写入每个点 diff --git a/Src/nmNum/nmSubWxs/nmWellPressureFlowPage.cpp b/Src/nmNum/nmSubWxs/nmWellPressureFlowPage.cpp index b6c15d5..fa07e3c 100644 --- a/Src/nmNum/nmSubWxs/nmWellPressureFlowPage.cpp +++ b/Src/nmNum/nmSubWxs/nmWellPressureFlowPage.cpp @@ -117,17 +117,14 @@ bool hasRealPhaseData(const QVector& vecPoints) return false; } -// 压力/流量记录下拉项文本:优先显示计量名称,否则回退到 Gauge Code;再拼接测量时间。 +// 压力/流量记录下拉框只显示稳定的 Gauge Code,避免顶部工具栏过宽。 +// 异常旧数据没有 Code 时,才回退到框架名称。 QString formatGaugeRecordLabel(const QString& sGaugeName, const QString& sGaugeCode, const QString& sGaugeTime) { - QString sBaseLabel = sGaugeName.isEmpty() ? sGaugeCode : sGaugeName; - if (sGaugeTime.isEmpty()) - { - return sBaseLabel; - } - return sBaseLabel + QLatin1String(" | ") + sGaugeTime; + Q_UNUSED(sGaugeTime); + return sGaugeCode.isEmpty() ? sGaugeName : sGaugeCode; } // 统一向压力/流量记录下拉框追加一项:身份无效的记录禁用不可选,空/损坏记录仍可选但追加提示后缀。 @@ -844,45 +841,6 @@ void nmWellPressureFlowPage::slotFlowChartPointClicked(double dTimePoint) findFlowSegmentByTableTime(dTableTimePoint)); } -void nmWellPressureFlowPage::slotPressureGaugeChanged(int nIndex) -{ - if (m_pWell == nullptr || nIndex < 0) - { - return; - } - - const QString sCode = m_pPressureGaugeCombo->itemData(nIndex).toString(); - if (sCode == m_pWell->getSelectedPressureGaugeCode()) - { - return; - } - - m_pWell->selectPressureGaugeCode(sCode); - refreshData(); -} - -void nmWellPressureFlowPage::slotFlowGaugeChanged(int nIndex) -{ - if (m_pWell == nullptr || nIndex < 0) - { - return; - } - - const QString sCode = m_pFlowGaugeCombo->itemData(nIndex).toString(); - if (sCode == m_pWell->getSelectedFlowGaugeCode()) - { - return; - } - - // 必须调用 switchSelectedFlowGaugeCode 而不是 selectFlowGaugeCode,才能根据新记录 - // 自己的 nIndexF 恢复/校验流动段选择,否则换记录后选择器就回到默认 0。 - m_pWell->switchSelectedFlowGaugeCode(sCode); - // 换记录后显示相重置为新记录的默认相,m_vecFlowGaugeRecords 仍为本井完整记录列表, - // 因此可在 refreshData() 重新填充下拉框前先用旧缓存安全查找新选中记录。 - m_eDisplayPhase = defaultDisplayPhase(currentFlowRecord()); - refreshData(); -} - void nmWellPressureFlowPage::slotDisplayPhaseChanged(int nIndex) { if (m_pWell == nullptr || nIndex < 0) @@ -1067,12 +1025,16 @@ void nmWellPressureFlowPage::createRecordSelectionBar() m_pPressureGaugeCombo = new QComboBox(m_pRecordSelectionBar); m_pPressureGaugeCombo->setEnabled(false); m_pPressureGaugeCombo->setMinimumWidth(160); + m_pPressureGaugeCombo->setToolTip( + tr("This page only displays the selected record.")); QLabel* pFlowLabel = new QLabel( tr("Flow Record:"), m_pRecordSelectionBar); m_pFlowGaugeCombo = new QComboBox(m_pRecordSelectionBar); m_pFlowGaugeCombo->setEnabled(false); m_pFlowGaugeCombo->setMinimumWidth(160); + m_pFlowGaugeCombo->setToolTip( + tr("This page only displays the selected record.")); // 显示相是纯展示状态,整体容器随记录是否为多相含数据而隐藏/显示。 m_pDisplayPhaseContainer = new QWidget(m_pRecordSelectionBar); @@ -1101,14 +1063,6 @@ void nmWellPressureFlowPage::createRecordSelectionBar() pBarLayout->addStretch(1); pBarLayout->addWidget(m_pCheckDataButton); - connect(m_pPressureGaugeCombo, - SIGNAL(currentIndexChanged(int)), - this, - SLOT(slotPressureGaugeChanged(int))); - connect(m_pFlowGaugeCombo, - SIGNAL(currentIndexChanged(int)), - this, - SLOT(slotFlowGaugeChanged(int))); connect(m_pDisplayPhaseCombo, SIGNAL(currentIndexChanged(int)), this, @@ -1138,10 +1092,11 @@ void nmWellPressureFlowPage::populatePressureGaugeCombo() QVector vecRecords = m_pWell->getPressureRecords(); - m_pPressureGaugeCombo->setEnabled(!vecRecords.isEmpty()); + m_pPressureGaugeCombo->setEnabled(false); + m_pPressureGaugeCombo->addItem(tr("None"), QString()); const QString sSelectedCode = m_pWell->getSelectedPressureGaugeCode(); - int nCurrentIndex = -1; + int nCurrentIndex = 0; for (int nIndex = 0; nIndex < vecRecords.size(); ++nIndex) { const nmPressureGaugeRecord& oRecord = vecRecords[nIndex]; @@ -1150,13 +1105,10 @@ void nmWellPressureFlowPage::populatePressureGaugeCombo() oRecord.sGaugeTime, oRecord.eStatus); if (oRecord.sGaugeCode == sSelectedCode) { - nCurrentIndex = nIndex; + nCurrentIndex = nIndex + 1; } } - if (nCurrentIndex >= 0) - { - m_pPressureGaugeCombo->setCurrentIndex(nCurrentIndex); - } + m_pPressureGaugeCombo->setCurrentIndex(nCurrentIndex); m_pPressureGaugeCombo->blockSignals(bSignalsWereBlocked); } @@ -1180,10 +1132,11 @@ void nmWellPressureFlowPage::populateFlowGaugeCombo() // 缓存整份记录列表供 currentFlowRecord() 安全返回内部指针,记录内容切换记录时不变。 m_vecFlowGaugeRecords = m_pWell->getFlowRecords(); - m_pFlowGaugeCombo->setEnabled(!m_vecFlowGaugeRecords.isEmpty()); + m_pFlowGaugeCombo->setEnabled(false); + m_pFlowGaugeCombo->addItem(tr("None"), QString()); const QString sSelectedCode = m_pWell->getSelectedFlowGaugeCode(); - int nCurrentIndex = -1; + int nCurrentIndex = 0; for (int nIndex = 0; nIndex < m_vecFlowGaugeRecords.size(); ++nIndex) { const nmFlowGaugeRecord& oRecord = m_vecFlowGaugeRecords[nIndex]; @@ -1192,13 +1145,10 @@ void nmWellPressureFlowPage::populateFlowGaugeCombo() oRecord.sGaugeTime, oRecord.eStatus); if (oRecord.sGaugeCode == sSelectedCode) { - nCurrentIndex = nIndex; + nCurrentIndex = nIndex + 1; } } - if (nCurrentIndex >= 0) - { - m_pFlowGaugeCombo->setCurrentIndex(nCurrentIndex); - } + m_pFlowGaugeCombo->setCurrentIndex(nCurrentIndex); m_pFlowGaugeCombo->blockSignals(bSignalsWereBlocked); } diff --git a/Src/nmNum/nmSubWxs/nmWxIncludeOtherWells.cpp b/Src/nmNum/nmSubWxs/nmWxIncludeOtherWells.cpp index f61de15..e270fcd 100644 --- a/Src/nmNum/nmSubWxs/nmWxIncludeOtherWells.cpp +++ b/Src/nmNum/nmSubWxs/nmWxIncludeOtherWells.cpp @@ -1,9 +1,13 @@ #include "nmWxIncludeOtherWells.h" #include "nmDataAnalyzeManager.h" #include "nmDataWellBase.h" + +#include #include -#include #include +#include +#include +#include namespace { @@ -20,16 +24,26 @@ bool isSupportedNumericalWell(nmDataWellBase* pWellData) eWellType == NM_WELL_MODEL::Horizontal_Fractured_Well; } -// 表格中的“产量”表示该相至少出现过一个非零流量;全零相仍会保留在 -// 求解输入中,但不能在摘要里误导用户为该井实际存在该相产量。 -bool hasNonZeroRate(nmDataWellBase* pWellData, NM_PHASE_TYPE ePhase) +// 从同一条流量记录中取得指定相的只读点集,避免三处分支判断各自漂移。 +const QVector& phasePoints( + const nmFlowGaugeRecord& oRecord, NM_PHASE_TYPE ePhase) { - if(pWellData == nullptr) { - return false; + switch(ePhase) { + case PHASE_Gas: + return oRecord.vecGasPoints; + case PHASE_Water: + return oRecord.vecWaterPoints; + case PHASE_Oil: + default: + return oRecord.vecOilPoints; } +} - const QVector vecPoints = - pWellData->getFlowSegmentPoints(ePhase); +// “有该相产量”按至少一个非零值判断;只有公共零占位行不算真实产量。 +bool hasNonZeroPhaseRate( + const nmFlowGaugeRecord& oRecord, NM_PHASE_TYPE ePhase) +{ + const QVector& vecPoints = phasePoints(oRecord, ePhase); for(int nIndex = 0; nIndex < vecPoints.size(); ++nIndex) { if(vecPoints[nIndex].y() != 0.0) { return true; @@ -37,6 +51,182 @@ bool hasNonZeroRate(nmDataWellBase* pWellData, NM_PHASE_TYPE ePhase) } return false; } + +// 下拉项只有在记录可用且该相确有非零产量时才允许选择。 +bool isSelectableFlowPhase( + const nmFlowGaugeRecord& oRecord, NM_PHASE_TYPE ePhase) +{ + return oRecord.eStatus == NM_GaugeRecord_Usable && + hasNonZeroPhaseRate(oRecord, ePhase); +} + +// 候选井至少要有一条可选分相记录;压力记录不是主动井候选的必要条件。 +bool hasSelectableRateRecord(const QVector& vecRecords) +{ + for(int nIndex = 0; nIndex < vecRecords.size(); ++nIndex) { + const nmFlowGaugeRecord& oRecord = vecRecords[nIndex]; + if(isSelectableFlowPhase(oRecord, PHASE_Oil) || + isSelectableFlowPhase(oRecord, PHASE_Gas) || + isSelectableFlowPhase(oRecord, PHASE_Water)) { + return true; + } + } + return false; +} + +// Gauge Code 是记录的稳定身份,显示名称和测量时间都不能替代它参与关联。 +const nmFlowGaugeRecord* findFlowRecord( + const QVector& vecRecords, + const QString& sGaugeCode) +{ + if(sGaugeCode.isEmpty()) { + return nullptr; + } + for(int nIndex = 0; nIndex < vecRecords.size(); ++nIndex) { + if(vecRecords[nIndex].sGaugeCode == sGaugeCode) { + return &vecRecords[nIndex]; + } + } + return nullptr; +} + +// 压力曲线至少需要两个点才能用于历史曲线计算。 +bool isSelectablePressureRecord(const nmPressureGaugeRecord& oRecord) +{ + return oRecord.eStatus == NM_GaugeRecord_Usable && + oRecord.vecPressurePoints.size() >= 2; +} + +// 打开对话框时只恢复仍存在且仍可用的压力记录 Code。 +bool containsSelectablePressureCode( + const QVector& vecRecords, + const QString& sGaugeCode) +{ + for(int nIndex = 0; nIndex < vecRecords.size(); ++nIndex) { + if(vecRecords[nIndex].sGaugeCode == sGaugeCode && + isSelectablePressureRecord(vecRecords[nIndex])) { + return true; + } + } + return false; +} + +// 下拉框只显示稳定的 Gauge Code,避免名称和测量时间挤占表格列宽。 +// 异常旧数据没有 Code 时,才回退到框架名称或“未命名”。 +QString gaugeRecordLabel(const QString& sGaugeName, + const QString& sGaugeCode, const QString& sGaugeTime) +{ + Q_UNUSED(sGaugeTime); + QString sLabel = sGaugeCode.isEmpty() ? sGaugeName : sGaugeCode; + if(sLabel.isEmpty()) { + sLabel = nmWxIncludeOtherWells::tr("(unnamed)"); + } + return sLabel; +} + +// 异常记录仍显示在下拉列表中,后缀用于说明其不可用于当前选择的原因。 +QString recordStatusSuffix(NM_GAUGE_RECORD_STATUS eStatus) +{ + switch(eStatus) { + case NM_GaugeRecord_Empty: + return nmWxIncludeOtherWells::tr(" (Empty)"); + case NM_GaugeRecord_Corrupt: + return nmWxIncludeOtherWells::tr(" (Corrupt)"); + case NM_GaugeRecord_InvalidIdentity: + return nmWxIncludeOtherWells::tr(" (Invalid)"); + default: + return QString(); + } +} + +// Qt 4.8 的 QComboBox 没有直接禁用单项的接口,通过其标准项模型设置标志。 +void setComboItemEnabled(QComboBox* pComboBox, int nIndex, bool bEnabled) +{ + QStandardItemModel* pModel = qobject_cast( + pComboBox == nullptr ? nullptr : pComboBox->model()); + QStandardItem* pItem = pModel == nullptr ? nullptr : pModel->item(nIndex); + if(pItem == nullptr) { + return; + } + if(bEnabled) { + pItem->setFlags(pItem->flags() | Qt::ItemIsEnabled); + } else { + pItem->setFlags(pItem->flags() & ~Qt::ItemIsEnabled); + } +} + +// 流动段索引是一基索引;统计段数时必须先排除框架可能写入的公共占位行。 +int flowRecordSegmentCount(const nmFlowGaugeRecord& oRecord) +{ + const QVector* pPoints = !oRecord.vecOilPoints.isEmpty() + ? &oRecord.vecOilPoints : (!oRecord.vecGasPoints.isEmpty() + ? &oRecord.vecGasPoints : (!oRecord.vecWaterPoints.isEmpty() + ? &oRecord.vecWaterPoints : nullptr)); + if(pPoints == nullptr) { + return 0; + } + + bool bHasPlaceholder = false; + if(oRecord.bMultiPhase) { + bHasPlaceholder = !oRecord.vecOilPoints.isEmpty() && + !oRecord.vecGasPoints.isEmpty() && + !oRecord.vecWaterPoints.isEmpty() && + oRecord.vecOilPoints.first() == QPointF(0.0, 0.0) && + oRecord.vecGasPoints.first() == QPointF(0.0, 0.0) && + oRecord.vecWaterPoints.first() == QPointF(0.0, 0.0); + } else { + bHasPlaceholder = pPoints->first() == QPointF(0.0, 0.0); + } + return pPoints->size() - (bHasPlaceholder ? 1 : 0); +} + +// 井别只用于只读展示和默认参考相判断,不由油、气、水下拉选择反推。 +QString wellCategoryText(NM_WELL_CATEGORY eWellCategory) +{ + switch(eWellCategory) { + case NM_WellCategory_Oil: + return nmWxIncludeOtherWells::tr("Oil well"); + case NM_WellCategory_Gas: + return nmWxIncludeOtherWells::tr("Gas well"); + case NM_WellCategory_Water: + return nmWxIncludeOtherWells::tr("Water well"); + default: + return nmWxIncludeOtherWells::tr("Unknown"); + } +} + +// WellDataRow 将三个固定相保存为独立开关,这里统一完成枚举到成员的映射。 +bool rowPhaseUsed(const WellDataRow& oData, NM_PHASE_TYPE ePhase) +{ + switch(ePhase) { + case PHASE_Oil: + return oData.m_bUseOilRate; + case PHASE_Gas: + return oData.m_bUseGasRate; + case PHASE_Water: + return oData.m_bUseWaterRate; + default: + return false; + } +} + +void setRowPhaseUsed( + WellDataRow& oData, NM_PHASE_TYPE ePhase, bool bUsed) +{ + switch(ePhase) { + case PHASE_Oil: + oData.m_bUseOilRate = bUsed; + break; + case PHASE_Gas: + oData.m_bUseGasRate = bUsed; + break; + case PHASE_Water: + oData.m_bUseWaterRate = bUsed; + break; + default: + break; + } +} } nmWxIncludeOtherWells::nmWxIncludeOtherWells( @@ -51,7 +241,7 @@ nmWxIncludeOtherWells::nmWxIncludeOtherWells( nmWxIncludeOtherWells::~nmWxIncludeOtherWells() { - // Qt的对象父子关系会自动处理内存释放 + // Qt 的对象父子关系会自动处理控件释放。 } void nmWxIncludeOtherWells::initUI() @@ -62,157 +252,389 @@ void nmWxIncludeOtherWells::initUI() this->setupConnections(); this->setWindowTitle(tr("Include Other Wells")); - this->resize(772, 339); + // 缩小时依靠横向滚动保留稳定列宽,不让中英文标题挤压下拉内容。 + this->setMinimumSize(760, 280); + this->resize(1240, 420); } void nmWxIncludeOtherWells::setupTable() { m_pTableWidget = new QTableWidget(this); - - QVector vecMapWells; - if(m_pDataManager != nullptr) { - vecMapWells = m_pDataManager->getWellDataList(); - } - - // 设置行列数 - m_pTableWidget->setColumnCount(6); - m_pTableWidget->setRowCount(vecMapWells.size()); - + m_pTableWidget->setColumnCount(ColumnCount); + m_pTableWidget->setRowCount(0); m_pTableWidget->setSelectionBehavior(QAbstractItemView::SelectRows); m_pTableWidget->setSelectionMode(QAbstractItemView::SingleSelection); - - m_pTableWidget->setStyleSheet( - "QTableWidget::item:focus {" - " background: transparent;" - " color: black;" - "}" - ); - - // 设置水平表头(列标题) - QStringList headers; - headers << tr("Name") << tr("Included") << tr("Oil production") - << tr("Gas production") << tr("Water production") << tr("Pressure"); - m_pTableWidget->setHorizontalHeaderLabels(headers); - // 确保水平表头可见 + m_pTableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_pTableWidget->setAlternatingRowColors(true); + m_pTableWidget->setWordWrap(false); + m_pTableWidget->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); + m_pTableWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + + QStringList listHeaders; + listHeaders << tr("Name") << tr("Well category") << tr("Included") + << tr("Oil production") << tr("Gas production") + << tr("Water production") << tr("Pressure") + << tr("Flow segment"); + m_pTableWidget->setHorizontalHeaderLabels(listHeaders); m_pTableWidget->horizontalHeader()->setVisible(true); + m_pTableWidget->horizontalHeader()->setMinimumSectionSize(64); + m_pTableWidget->horizontalHeader()->setStretchLastSection(false); + m_pTableWidget->horizontalHeader()->setResizeMode( + ColumnName, QHeaderView::Stretch); + m_pTableWidget->horizontalHeader()->setResizeMode( + ColumnWellCategory, QHeaderView::ResizeToContents); + m_pTableWidget->horizontalHeader()->setResizeMode( + ColumnIncluded, QHeaderView::Fixed); + m_pTableWidget->horizontalHeader()->setResizeMode( + ColumnOilRate, QHeaderView::Interactive); + m_pTableWidget->horizontalHeader()->setResizeMode( + ColumnGasRate, QHeaderView::Interactive); + m_pTableWidget->horizontalHeader()->setResizeMode( + ColumnWaterRate, QHeaderView::Interactive); + m_pTableWidget->horizontalHeader()->setResizeMode( + ColumnPressure, QHeaderView::Interactive); + m_pTableWidget->horizontalHeader()->setResizeMode( + ColumnFlowSegment, QHeaderView::Fixed); + m_pTableWidget->setColumnWidth(ColumnIncluded, 76); + m_pTableWidget->setColumnWidth(ColumnOilRate, 180); + m_pTableWidget->setColumnWidth(ColumnGasRate, 180); + m_pTableWidget->setColumnWidth(ColumnWaterRate, 180); + m_pTableWidget->setColumnWidth(ColumnPressure, 190); + m_pTableWidget->setColumnWidth(ColumnFlowSegment, 96); + + m_pTableWidget->verticalHeader()->setVisible(false); + m_pTableWidget->verticalHeader()->setDefaultSectionSize(32); + m_pTableWidget->verticalHeader()->setMinimumSectionSize(28); - // 设置垂直表头(行标题)属性 - m_pTableWidget->verticalHeader()->setVisible(true); // 保持垂直表头可见(用于行选择视觉反馈) - m_pTableWidget->verticalHeader()->setMinimumWidth(16); // 最小宽度 - - // 隐藏垂直表头的数字(设置为空文本) - QStringList emptyRowHeaders; - for(int i = 0; i < m_pTableWidget->rowCount(); ++i) { - emptyRowHeaders << ""; + QVector vecMapWells; + if(m_pDataManager != nullptr) { + vecMapWells = m_pDataManager->getWellDataList(); } - m_pTableWidget->setVerticalHeaderLabels(emptyRowHeaders); - - // 加载数据 - QString sPrimaryWellCode = m_pDataManager != nullptr + const QString sPrimaryWellCode = m_pDataManager != nullptr ? m_pDataManager->getPrimaryWellCode() : QString(); - int nDisplayRow = 0; - // 第一步:候选来源是 Map,而不是整个项目井库。该对话框只管理 - // 其他有产量的生产/注入井;主井和无产量观察井都不在这里显示。 - for(int i = 0; i < vecMapWells.size(); i++) { - nmDataWellBase* pWell = vecMapWells[i]; - if(pWell == nullptr || !isSupportedNumericalWell(pWell)) { + // 候选来源仍严格限定为 Map 中的其他主动井;只把选择界面迁移到这里。 + for(int nIndex = 0; nIndex < vecMapWells.size(); ++nIndex) { + nmDataWellBase* pWellData = vecMapWells[nIndex]; + if(!isSupportedNumericalWell(pWellData) || + pWellData->getWellCode() == sPrimaryWellCode || + !nmIsValidWellCategory(pWellData->getWellCategory())) { continue; } - const bool bIsPrimary = pWell->getWellCode() == sPrimaryWellCode; - const bool bHasValidRate = - nmIsValidWellCategory(pWell->getWellCategory()) && - pWell->getFlowSegmentCount() > 0; - const bool bHasValidPressure = pWell->getPressurePoints().size() >= 2; - if(bIsPrimary || !bHasValidRate) { + WellDataRow oWellRow; + oWellRow.m_sWellCode = pWellData->getWellCode(); + oWellRow.m_sName = pWellData->getWellName(); + oWellRow.m_bIsPrimary = false; + oWellRow.m_eWellCategory = pWellData->getWellCategory(); + oWellRow.m_eMode = NM_CaseWell_RateControlled; + oWellRow.m_vecPressureRecords = pWellData->getPressureRecords(); + oWellRow.m_vecFlowRecords = pWellData->getFlowRecords(); + oWellRow.m_bCanCalculate = + hasSelectableRateRecord(oWellRow.m_vecFlowRecords); + if(!oWellRow.m_bCanCalculate) { continue; } - WellDataRow oWellRow; - oWellRow.m_sWellCode = pWell->getWellCode(); - oWellRow.m_sName = pWell->getWellName(); - oWellRow.m_bIsPrimary = bIsPrimary; - oWellRow.m_eWellCategory = pWell->getWellCategory(); - - // 第二步:读取这口有产量干扰井上次确认的包含状态。 - oWellRow.m_bCanCalculate = true; - oWellRow.m_bIsIncluded = - m_pDataManager != nullptr && - m_pDataManager->getNumericalAnalysisCase()->isIncludedWell( - oWellRow.m_sWellCode); - - // 第三步:多相井可同时具有多列产量,逐相读取实际非零流量。 - oWellRow.m_sPressure = bHasValidPressure - ? tr("History") : tr("None"); - oWellRow.m_sOilProd = tr("None"); - oWellRow.m_sGasProd = tr("None"); - oWellRow.m_sWaterProd = tr("None"); - if(hasNonZeroRate(pWell, PHASE_Oil)) { - oWellRow.m_sOilProd = tr("Rate"); - } - if(hasNonZeroRate(pWell, PHASE_Gas)) { - oWellRow.m_sGasProd = tr("Rate"); + oWellRow.m_sPressureGaugeCode = + pWellData->getSelectedPressureGaugeCode(); + if(!containsSelectablePressureCode( + oWellRow.m_vecPressureRecords, + oWellRow.m_sPressureGaugeCode)) { + oWellRow.m_sPressureGaugeCode.clear(); } - if(hasNonZeroRate(pWell, PHASE_Water)) { - oWellRow.m_sWaterProd = tr("Rate"); + oWellRow.m_sFlowGaugeCode = + pWellData->getSelectedFlowGaugeCode(); + oWellRow.m_bUseOilRate = pWellData->getUseOilRate(); + oWellRow.m_bUseGasRate = pWellData->getUseGasRate(); + oWellRow.m_bUseWaterRate = pWellData->getUseWaterRate(); + oWellRow.m_nFlowSegmentIndex = pWellData->getIndexF(); + + const bool bHadRequestedPhase = oWellRow.m_bUseOilRate || + oWellRow.m_bUseGasRate || oWellRow.m_bUseWaterRate; + normalizeFlowSelection(oWellRow); + // 新井默认启用三相;若框架当前选择失效,回退到第一条有产量记录。 + // 用户已明确把三相都设为“无”时不自动恢复。 + if(!oWellRow.canCalculate() && bHadRequestedPhase) { + for(int nRecordIndex = 0; + nRecordIndex < oWellRow.m_vecFlowRecords.size(); + ++nRecordIndex) { + const nmFlowGaugeRecord& oRecord = + oWellRow.m_vecFlowRecords[nRecordIndex]; + if(oRecord.eStatus != NM_GaugeRecord_Usable) { + continue; + } + oWellRow.m_sFlowGaugeCode = oRecord.sGaugeCode; + oWellRow.m_bUseOilRate = + isSelectableFlowPhase(oRecord, PHASE_Oil); + oWellRow.m_bUseGasRate = + isSelectableFlowPhase(oRecord, PHASE_Gas); + oWellRow.m_bUseWaterRate = + isSelectableFlowPhase(oRecord, PHASE_Water); + normalizeFlowSelection(oWellRow); + if(oWellRow.canCalculate()) { + break; + } + } } - oWellRow.m_eMode = NM_CaseWell_RateControlled; - // 添加到数据列表和表格 + oWellRow.m_bIsIncluded = oWellRow.canCalculate() && + m_pDataManager->getNumericalAnalysisCase()->isIncludedWell( + oWellRow.m_sWellCode); + + const int nRow = m_vecWellData.size(); m_vecWellData.append(oWellRow); - addWellDataToTable(oWellRow, nDisplayRow); - ++nDisplayRow; + m_pTableWidget->setRowCount(nRow + 1); + addWellDataToTable(oWellRow, nRow); } - m_pTableWidget->setRowCount(nDisplayRow); // 调整实际显示的行数 - m_pTableWidget->horizontalHeader()->setStretchLastSection(true); - m_pTableWidget->horizontalHeader()->setResizeMode(QHeaderView::Stretch); + if(m_vecWellData.isEmpty()) { + m_pTableWidget->setRowCount(1); + m_pTableWidget->setSpan(0, 0, 1, ColumnCount); + QTableWidgetItem* pEmptyItem = new QTableWidgetItem( + tr("No eligible wells")); + pEmptyItem->setFlags(Qt::NoItemFlags); + pEmptyItem->setTextAlignment(Qt::AlignCenter); + m_pTableWidget->setItem(0, 0, pEmptyItem); + } } -void nmWxIncludeOtherWells::addWellDataToTable(const WellDataRow& data, int row) +void nmWxIncludeOtherWells::addWellDataToTable( + const WellDataRow& data, int row) { - // Name列 - 不可编辑 - QTableWidgetItem* nameItem = new QTableWidgetItem(data.m_sName); - nameItem->setFlags(nameItem->flags() & ~Qt::ItemIsEditable); - nameItem->setData(Qt::UserRole, data.m_sWellCode); // 表格内部始终携带 WellCode - nameItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); - m_pTableWidget->setItem(row, 0, nameItem); + QTableWidgetItem* pNameItem = new QTableWidgetItem(data.m_sName); + pNameItem->setData(Qt::UserRole, data.m_sWellCode); + pNameItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + m_pTableWidget->setItem(row, ColumnName, pNameItem); + + QTableWidgetItem* pCategoryItem = new QTableWidgetItem( + wellCategoryText(data.m_eWellCategory)); + pCategoryItem->setTextAlignment(Qt::AlignCenter); + m_pTableWidget->setItem(row, ColumnWellCategory, pCategoryItem); + + QWidget* pCheckContainer = new QWidget(m_pTableWidget); + QCheckBox* pCheckBox = new QCheckBox(pCheckContainer); + QHBoxLayout* pCheckLayout = new QHBoxLayout(pCheckContainer); + pCheckLayout->setContentsMargins(0, 0, 0, 0); + pCheckLayout->setAlignment(Qt::AlignCenter); + pCheckLayout->addWidget(pCheckBox); + pCheckBox->setProperty("row", row); + connect(pCheckBox, SIGNAL(stateChanged(int)), + this, SLOT(onCheckBoxStateChanged(int))); + m_pTableWidget->setCellWidget(row, ColumnIncluded, pCheckContainer); + + const int arrRateColumns[] = { + ColumnOilRate, ColumnGasRate, ColumnWaterRate + }; + const NM_PHASE_TYPE arrPhases[] = { + PHASE_Oil, PHASE_Gas, PHASE_Water + }; + for(int nIndex = 0; nIndex < 3; ++nIndex) { + QComboBox* pRateCombo = new QComboBox(m_pTableWidget); + pRateCombo->setProperty("row", row); + pRateCombo->setProperty("phase", static_cast(arrPhases[nIndex])); + pRateCombo->setMaxVisibleItems(16); + connect(pRateCombo, SIGNAL(currentIndexChanged(int)), + this, SLOT(onRateSourceChanged(int))); + m_pTableWidget->setCellWidget(row, arrRateColumns[nIndex], pRateCombo); + } - // Included列 - 使用自定义复选框组件 - QWidget* pWidget = new QWidget(); - QCheckBox* pCheckBox = new QCheckBox(); - QHBoxLayout* pLayout = new QHBoxLayout(pWidget); - pLayout->addWidget(pCheckBox); - pLayout->setAlignment(Qt::AlignCenter); - pLayout->setContentsMargins(0, 0, 0, 0); - pWidget->setLayout(pLayout); + QComboBox* pPressureCombo = new QComboBox(m_pTableWidget); + pPressureCombo->setProperty("row", row); + pPressureCombo->setMaxVisibleItems(16); + connect(pPressureCombo, SIGNAL(currentIndexChanged(int)), + this, SLOT(onPressureSourceChanged(int))); + m_pTableWidget->setCellWidget(row, ColumnPressure, pPressureCombo); - pCheckBox->setChecked(data.m_bIsIncluded); - // 表内只有其他有产量井,用户可以决定它是否作为干扰井参与计算。 - pCheckBox->setEnabled(data.canCalculate()); - pCheckBox->setProperty("row", row); // 存储行索引 + QComboBox* pSegmentCombo = new QComboBox(m_pTableWidget); + pSegmentCombo->setProperty("row", row); + connect(pSegmentCombo, SIGNAL(currentIndexChanged(int)), + this, SLOT(onFlowSegmentChanged(int))); + m_pTableWidget->setCellWidget(row, ColumnFlowSegment, pSegmentCombo); - // 连接复选框信号 - connect(pCheckBox, SIGNAL(stateChanged(int)), this, SLOT(onCheckBoxStateChanged(int))); + refreshRowEditors(row); +} - // 设置单元格控件 - m_pTableWidget->setCellWidget(row, 1, pWidget); +void nmWxIncludeOtherWells::normalizeFlowSelection(WellDataRow& data) const +{ + const nmFlowGaugeRecord* pRecord = findFlowRecord( + data.m_vecFlowRecords, data.m_sFlowGaugeCode); + if(pRecord == nullptr || + pRecord->eStatus != NM_GaugeRecord_Usable) { + data.m_sFlowGaugeCode.clear(); + data.m_bUseOilRate = false; + data.m_bUseGasRate = false; + data.m_bUseWaterRate = false; + data.m_nFlowSegmentIndex = 0; + return; + } + + data.m_bUseOilRate = data.m_bUseOilRate && + isSelectableFlowPhase(*pRecord, PHASE_Oil); + data.m_bUseGasRate = data.m_bUseGasRate && + isSelectableFlowPhase(*pRecord, PHASE_Gas); + data.m_bUseWaterRate = data.m_bUseWaterRate && + isSelectableFlowPhase(*pRecord, PHASE_Water); + if(!data.m_bUseOilRate && !data.m_bUseGasRate && + !data.m_bUseWaterRate) { + data.m_sFlowGaugeCode.clear(); + data.m_nFlowSegmentIndex = 0; + return; + } + + const int nSegmentCount = flowRecordSegmentCount(*pRecord); + if(nSegmentCount <= 0) { + data.m_sFlowGaugeCode.clear(); + data.m_bUseOilRate = false; + data.m_bUseGasRate = false; + data.m_bUseWaterRate = false; + data.m_nFlowSegmentIndex = 0; + return; + } + if(data.m_nFlowSegmentIndex < 1 || + data.m_nFlowSegmentIndex > nSegmentCount) { + data.m_nFlowSegmentIndex = pRecord->nIndexF; + if(data.m_nFlowSegmentIndex < 1 || + data.m_nFlowSegmentIndex > nSegmentCount) { + data.m_nFlowSegmentIndex = nSegmentCount; + } + } +} + +void nmWxIncludeOtherWells::populateRateCombo( + QComboBox* pComboBox, const WellDataRow& data, + NM_PHASE_TYPE ePhase) const +{ + if(pComboBox == nullptr) { + return; + } + const bool bSignalsWereBlocked = pComboBox->blockSignals(true); + pComboBox->clear(); + pComboBox->addItem(tr("None"), QString()); + + // 三个分相下拉框都列出完整记录集合,但当前相无数据的记录保持可见且禁用。 + for(int nIndex = 0; nIndex < data.m_vecFlowRecords.size(); ++nIndex) { + const nmFlowGaugeRecord& oRecord = data.m_vecFlowRecords[nIndex]; + const bool bSelectable = isSelectableFlowPhase(oRecord, ePhase); + QString sLabel = gaugeRecordLabel( + oRecord.sGaugeName, oRecord.sGaugeCode, oRecord.sGaugeTime); + sLabel += recordStatusSuffix(oRecord.eStatus); + if(oRecord.eStatus == NM_GaugeRecord_Usable && !bSelectable) { + sLabel += tr(" (No data)"); + } + pComboBox->addItem(sLabel, oRecord.sGaugeCode); + setComboItemEnabled(pComboBox, pComboBox->count() - 1, bSelectable); + } + + int nCurrentIndex = 0; + if(rowPhaseUsed(data, ePhase)) { + const int nFoundIndex = pComboBox->findData(data.m_sFlowGaugeCode); + if(nFoundIndex > 0) { + nCurrentIndex = nFoundIndex; + } + } + pComboBox->setCurrentIndex(nCurrentIndex); + pComboBox->setToolTip(pComboBox->currentText()); + pComboBox->blockSignals(bSignalsWereBlocked); +} + +void nmWxIncludeOtherWells::populatePressureCombo( + QComboBox* pComboBox, const WellDataRow& data) const +{ + if(pComboBox == nullptr) { + return; + } + const bool bSignalsWereBlocked = pComboBox->blockSignals(true); + pComboBox->clear(); + pComboBox->addItem(tr("None"), QString()); + for(int nIndex = 0; nIndex < data.m_vecPressureRecords.size(); ++nIndex) { + const nmPressureGaugeRecord& oRecord = data.m_vecPressureRecords[nIndex]; + const bool bSelectable = isSelectablePressureRecord(oRecord); + QString sLabel = gaugeRecordLabel( + oRecord.sGaugeName, oRecord.sGaugeCode, oRecord.sGaugeTime); + sLabel += recordStatusSuffix(oRecord.eStatus); + if(oRecord.eStatus == NM_GaugeRecord_Usable && !bSelectable) { + sLabel += tr(" (No data)"); + } + pComboBox->addItem(sLabel, oRecord.sGaugeCode); + setComboItemEnabled(pComboBox, pComboBox->count() - 1, bSelectable); + } + + const int nCurrentIndex = pComboBox->findData( + data.m_sPressureGaugeCode); + pComboBox->setCurrentIndex(nCurrentIndex < 0 ? 0 : nCurrentIndex); + pComboBox->setToolTip(pComboBox->currentText()); + pComboBox->blockSignals(bSignalsWereBlocked); +} + +void nmWxIncludeOtherWells::populateFlowSegmentCombo( + QComboBox* pComboBox, const WellDataRow& data) const +{ + if(pComboBox == nullptr) { + return; + } + const bool bSignalsWereBlocked = pComboBox->blockSignals(true); + pComboBox->clear(); + const nmFlowGaugeRecord* pRecord = findFlowRecord( + data.m_vecFlowRecords, data.m_sFlowGaugeCode); + const int nSegmentCount = pRecord == nullptr + ? 0 : flowRecordSegmentCount(*pRecord); + for(int nIndex = 1; nIndex <= nSegmentCount; ++nIndex) { + pComboBox->addItem(tr("Segment %1").arg(nIndex), nIndex); + } + const int nCurrentIndex = pComboBox->findData( + data.m_nFlowSegmentIndex); + if(nCurrentIndex >= 0) { + pComboBox->setCurrentIndex(nCurrentIndex); + } + pComboBox->setEnabled(data.canCalculate()); + pComboBox->setToolTip(pComboBox->currentText()); + pComboBox->blockSignals(bSignalsWereBlocked); +} - // 其他列只显示各类 simulation target,与 Saphir 对话框保持一致。 - QStringList listOtherData; - listOtherData.append(data.m_sOilProd); - listOtherData.append(data.m_sGasProd); - listOtherData.append(data.m_sWaterProd); - listOtherData.append(data.m_sPressure); +void nmWxIncludeOtherWells::refreshRowEditors(int row) +{ + if(row < 0 || row >= m_vecWellData.size()) { + return; + } + const WellDataRow& oData = m_vecWellData[row]; + populateRateCombo(qobject_cast( + m_pTableWidget->cellWidget(row, ColumnOilRate)), + oData, PHASE_Oil); + populateRateCombo(qobject_cast( + m_pTableWidget->cellWidget(row, ColumnGasRate)), + oData, PHASE_Gas); + populateRateCombo(qobject_cast( + m_pTableWidget->cellWidget(row, ColumnWaterRate)), + oData, PHASE_Water); + populatePressureCombo(qobject_cast( + m_pTableWidget->cellWidget(row, ColumnPressure)), oData); + populateFlowSegmentCombo(qobject_cast( + m_pTableWidget->cellWidget(row, ColumnFlowSegment)), oData); + updateIncludeCheckBox(row); +} - for(int col = 2; col < 6; col++) { - QTableWidgetItem* item = new QTableWidgetItem(listOtherData[col-2]); - item->setFlags(item->flags() & ~Qt::ItemIsEditable); - item->setData(Qt::UserRole, row); - item->setTextAlignment(Qt::AlignCenter); - m_pTableWidget->setItem(row, col, item); +void nmWxIncludeOtherWells::updateIncludeCheckBox(int row) +{ + if(row < 0 || row >= m_vecWellData.size()) { + return; + } + WellDataRow& oData = m_vecWellData[row]; + if(!oData.canCalculate()) { + oData.m_bIsIncluded = false; + } + QWidget* pContainer = m_pTableWidget->cellWidget(row, ColumnIncluded); + QCheckBox* pCheckBox = pContainer == nullptr + ? nullptr : pContainer->findChild(); + if(pCheckBox == nullptr) { + return; } + const bool bSignalsWereBlocked = pCheckBox->blockSignals(true); + pCheckBox->setEnabled(oData.m_bCanCalculate); + pCheckBox->setChecked(oData.m_bIsIncluded); + pCheckBox->setToolTip(oData.canCalculate() + ? QString() : tr("Select at least one production record first.")); + pCheckBox->blockSignals(bSignalsWereBlocked); } void nmWxIncludeOtherWells::onCheckBoxStateChanged(int state) @@ -221,25 +643,91 @@ void nmWxIncludeOtherWells::onCheckBoxStateChanged(int state) if(pCheckBox == nullptr) { return; } - const int nRow = pCheckBox->property("row").toInt(); if(nRow < 0 || nRow >= m_vecWellData.size()) { return; } - // 主井正常不会进入本表;保留该判断作为防御,避免未来改动误取消主井。 - if(m_vecWellData[nRow].m_bIsPrimary) { - m_vecWellData[nRow].m_bIsIncluded = true; + WellDataRow& oData = m_vecWellData[nRow]; + if(oData.m_bIsPrimary || + (state == Qt::Checked && !oData.canCalculate())) { + oData.m_bIsIncluded = false; + updateIncludeCheckBox(nRow); + return; + } + oData.m_bIsIncluded = state == Qt::Checked; + m_pTableWidget->selectRow(nRow); +} + +void nmWxIncludeOtherWells::onRateSourceChanged(int nIndex) +{ + QComboBox* pComboBox = qobject_cast(sender()); + if(pComboBox == nullptr || nIndex < 0) { + return; + } + const int nRow = pComboBox->property("row").toInt(); + const NM_PHASE_TYPE ePhase = static_cast( + pComboBox->property("phase").toInt()); + if(nRow < 0 || nRow >= m_vecWellData.size()) { return; } - // 其他候选井只记录选择状态,绝不在该对话框内创建或删除 Map 井。 - m_vecWellData[nRow].m_bIsIncluded = (state == Qt::Checked); + WellDataRow& oData = m_vecWellData[nRow]; + // 可见文本只用于展示,真正写回始终读取 itemData 中的 Gauge Code。 + const QString sGaugeCode = pComboBox->itemData(nIndex).toString(); + if(sGaugeCode.isEmpty()) { + setRowPhaseUsed(oData, ePhase, false); + } else { + const nmFlowGaugeRecord* pRecord = findFlowRecord( + oData.m_vecFlowRecords, sGaugeCode); + if(pRecord == nullptr || !isSelectableFlowPhase(*pRecord, ePhase)) { + refreshRowEditors(nRow); + return; + } + if(oData.m_sFlowGaugeCode != sGaugeCode) { + oData.m_sFlowGaugeCode = sGaugeCode; + oData.m_nFlowSegmentIndex = pRecord->nIndexF; + } + setRowPhaseUsed(oData, ePhase, true); + } + + // PEBI 每口井只有一条 Rate.t。切换任一相的记录后,其他已启用相 + // 自动使用同一记录;新记录没有的相自动回到“无”。 + normalizeFlowSelection(oData); + refreshRowEditors(nRow); + m_pTableWidget->selectRow(nRow); +} + +void nmWxIncludeOtherWells::onPressureSourceChanged(int nIndex) +{ + QComboBox* pComboBox = qobject_cast(sender()); + if(pComboBox == nullptr || nIndex < 0) { + return; + } + const int nRow = pComboBox->property("row").toInt(); + if(nRow < 0 || nRow >= m_vecWellData.size()) { + return; + } + m_vecWellData[nRow].m_sPressureGaugeCode = + pComboBox->itemData(nIndex).toString(); + pComboBox->setToolTip(pComboBox->currentText()); m_pTableWidget->selectRow(nRow); } -void nmWxIncludeOtherWells::onItemClicked(QTableWidgetItem* item) +void nmWxIncludeOtherWells::onFlowSegmentChanged(int nIndex) { + QComboBox* pComboBox = qobject_cast(sender()); + if(pComboBox == nullptr || nIndex < 0) { + return; + } + const int nRow = pComboBox->property("row").toInt(); + if(nRow < 0 || nRow >= m_vecWellData.size()) { + return; + } + m_vecWellData[nRow].m_nFlowSegmentIndex = + pComboBox->itemData(nIndex).toInt(); + pComboBox->setToolTip(pComboBox->currentText()); + m_pTableWidget->selectRow(nRow); } QVector nmWxIncludeOtherWells::getWellData() const @@ -249,24 +737,20 @@ QVector nmWxIncludeOtherWells::getWellData() const void nmWxIncludeOtherWells::setupButtons() { - // 创建确定和取消按钮 m_pOkButton = new QPushButton(tr("OK"), this); m_pCancelButton = new QPushButton(tr("Cancel"), this); } void nmWxIncludeOtherWells::setupLayouts() { - // 创建按钮布局并添加按钮 m_pButtonLayout = new QHBoxLayout(); - m_pButtonLayout->addStretch(); // 添加伸缩项使按钮靠右 + m_pButtonLayout->addStretch(); m_pButtonLayout->addWidget(m_pOkButton); m_pButtonLayout->addWidget(m_pCancelButton); - // 创建主布局并添加控件 m_pMainLayout = new QVBoxLayout(this); m_pMainLayout->addWidget(m_pTableWidget); m_pMainLayout->addLayout(m_pButtonLayout); - setLayout(m_pMainLayout); } @@ -274,6 +758,4 @@ void nmWxIncludeOtherWells::setupConnections() { connect(m_pOkButton, SIGNAL(clicked()), this, SLOT(accept())); connect(m_pCancelButton, SIGNAL(clicked()), this, SLOT(reject())); - connect(m_pTableWidget, SIGNAL(itemClicked(QTableWidgetItem*)), - this, SLOT(onItemClicked(QTableWidgetItem*))); } diff --git a/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp b/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp index 3e781e4..108f826 100644 --- a/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp +++ b/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp @@ -64,6 +64,17 @@ bool isSupportedAdditionalWell(nmDataWellBase* pWellData) eWellType == Horizontal_Fractured_Well; } +// 可配置性看框架记录本身,不能看当前是否已把三个相都设为“无”, +// 否则用户清空分相后将无法再次打开对话框恢复选择。 +bool hasAvailableRateTarget(nmDataWellBase* pWellData) +{ + if(pWellData == nullptr || + !nmIsValidWellCategory(pWellData->getWellCategory())) { + return false; + } + return pWellData->hasAvailableFlowRate(); +} + } nmWxNumericalDesign* nmWxNumericalDesign::s_pCurrentInstance = nullptr; @@ -1080,51 +1091,88 @@ void nmWxNumericalDesign::onOptionsIconClicked() if(dlg.exec() == QDialog::Accepted) { QVector vecWellRows = dlg.getWellData(); QVector vecIncludedWells; + bool bIncludedMembershipChanged = false; + bool bIncludedCurveSelectionChanged = false; - // 第一步:这里只收集 Map 中被勾选的有产量干扰井。主井由分析方案 - // 单独保存;无产量观察井由数据管理层自动加入,与本对话框无关。 + // 第一步:先把每一行的压力、公共流量记录、分相开关和流动段写回。 + // 这些状态属于井本身;未勾选井也保留配置,方便下次直接启用。 for(int nRowIndex = 0; nRowIndex < vecWellRows.size(); ++nRowIndex) { const WellDataRow& oWellRow = vecWellRows[nRowIndex]; - if(oWellRow.m_bIsPrimary || - !oWellRow.m_bIsIncluded || - !oWellRow.canCalculate()) { + if(oWellRow.m_bIsPrimary) { continue; } nmDataWellBase* pWellData = m_pDataManager->findWellByCode(oWellRow.m_sWellCode); if(pWellData == nullptr) { - // Map 中已经移除的井不能被 Include 对话框重新创建。 continue; } - if(!nmIsValidWellCategory(pWellData->getWellCategory()) || - pWellData->getFlowSegmentCount() <= 0) { - // 对话框打开后若产量数据发生变化,提交时仍要阻止它混入包含列表。 - continue; - } - - vecIncludedWells.append( - nmCalculationWellRef(oWellRow.m_sWellCode, - NM_CaseWell_RateControlled)); - - // 第二步:为被包含的有产量井同步历史压力、双对数和半对数数据。 - QVector > vecHistoryPressure; - QVector > vecHistoryLogLog; - QVector > vecHistorySemiLog; - m_pDataManager->calculationLogData(pWellData, - vecHistoryPressure, - vecHistoryLogLog, - vecHistorySemiLog); - pWellData->setHistoryPressure(vecHistoryPressure); - pWellData->setHistoryLogLog(vecHistoryLogLog); - pWellData->setHistorySemiLog(vecHistorySemiLog); + const bool bWasIncluded = + m_pDataManager->getNumericalAnalysisCase()->isIncludedWell( + oWellRow.m_sWellCode); + const bool bWillBeIncluded = oWellRow.m_bIsIncluded && + oWellRow.canCalculate(); + bIncludedMembershipChanged = bIncludedMembershipChanged || + bWasIncluded != bWillBeIncluded; + + // 必须在写回前冻结差异;写回后井对象已无法提供旧的记录和分相状态。 + const bool bCurveSelectionChanged = + pWellData->getSelectedPressureGaugeCode() != + oWellRow.m_sPressureGaugeCode || + pWellData->getSelectedFlowGaugeCode() != + oWellRow.m_sFlowGaugeCode || + pWellData->getUseOilRate() != oWellRow.m_bUseOilRate || + pWellData->getUseGasRate() != oWellRow.m_bUseGasRate || + pWellData->getUseWaterRate() != oWellRow.m_bUseWaterRate || + pWellData->getIndexF() != oWellRow.m_nFlowSegmentIndex; + + // 先切换公共流量记录,再写入分相开关和该记录的一基流动段索引。 + // 这样 switchSelectedFlowGaugeCode() 可以先保存旧记录自己的段索引。 + pWellData->selectPressureGaugeCode( + oWellRow.m_sPressureGaugeCode); + if(pWellData->getSelectedFlowGaugeCode() != + oWellRow.m_sFlowGaugeCode) { + pWellData->switchSelectedFlowGaugeCode( + oWellRow.m_sFlowGaugeCode); + } + pWellData->setUseOilRate(oWellRow.m_bUseOilRate); + pWellData->setUseGasRate(oWellRow.m_bUseGasRate); + pWellData->setUseWaterRate(oWellRow.m_bUseWaterRate); + pWellData->setIndexF(oWellRow.m_nFlowSegmentIndex); + + if(bCurveSelectionChanged) { + QVector > vecHistoryPressure; + QVector > vecHistoryLogLog; + QVector > vecHistorySemiLog; + m_pDataManager->calculationLogData(pWellData, + vecHistoryPressure, + vecHistoryLogLog, + vecHistorySemiLog); + pWellData->setHistoryPressure(vecHistoryPressure); + pWellData->setHistoryLogLog(vecHistoryLogLog); + pWellData->setHistorySemiLog(vecHistorySemiLog); + if(bWasIncluded || bWillBeIncluded) { + bIncludedCurveSelectionChanged = true; + } + } + + if(bWillBeIncluded) { + vecIncludedWells.append( + nmCalculationWellRef(oWellRow.m_sWellCode, + NM_CaseWell_RateControlled)); + } } - // 第三步:一次性提交选择,内部会使旧网格和旧结果失效。 + // 第二步:井集合改变会使网格和结果一起失效;仅更换曲线或分相时 + // 保留现有网格,只使旧求解结果失效。 m_pDataManager->setIncludedCalculationWells(vecIncludedWells); m_pDataManager->setIncludeOtherWells( m_pIncludeOtherWellsCheck->isChecked()); + if(bIncludedCurveSelectionChanged && + !bIncludedMembershipChanged) { + m_pDataManager->invalidatePebiResults(); + } nmWxParameterProperty::notifyUpdateTable(); fillResultWellCombo(); @@ -1215,9 +1263,7 @@ void nmWxNumericalDesign::updateIncludeOtherWellsAvailability() continue; } - const bool bHasRateTarget = - nmIsValidWellCategory(pWellData->getWellCategory()) && - pWellData->getFlowSegmentCount() > 0; + const bool bHasRateTarget = hasAvailableRateTarget(pWellData); if(bHasRateTarget) { bHasEligibleOtherWell = true; break;