#include "nmWellPressureFlowPage.h" #include "iUnitGroup.h" #include "iUnitHelper.h" #include "iUnitItem.h" #include "nmDataAnalyzeContext.h" #include "nmDataAnalyzeContextProvider.h" #include "nmDataAnalyzeManager.h" #include "nmDataWellBase.h" #include "nmWxPressFlowChartWidget.h" #include "ZxBaseUtil.h" #include "ZxBaHelper.h" #include "ZxDataGaugeF.h" #include "ZxDataProject.h" #include "ZxDataWell.h" #include "ZxTableHeaderViewUnit.h" #include "zxSysUtils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { QString preferredTimeUnit() { return QLatin1String("h"); } QString preferredFlowUnit() { return QString::fromUtf8("m\xC2\xB3/d"); } iUnitGroup* resolveUnitGroup(const QStringList& listCandidates, QString& sResolvedUnit) { for (int nIndex = 0; nIndex < listCandidates.size(); ++nIndex) { const QString& sCandidate = listCandidates[nIndex]; iUnitGroup* pUnitGroup = iUnitHelper::getUnitGroupByUnit(sCandidate); if (pUnitGroup == nullptr) { continue; } int nUnitIndex = pUnitGroup->indexOf(sCandidate); QStringList listUnitNames = pUnitGroup->getAllUnitNames(); if (nUnitIndex < 0 || nUnitIndex >= listUnitNames.size()) { continue; } sResolvedUnit = listUnitNames[nUnitIndex]; return pUnitGroup; } sResolvedUnit = listCandidates.isEmpty() ? QString() : listCandidates.first(); return nullptr; } QLabel* createEmptyLabel(const QString& sText, QWidget* pParent) { QLabel* pLabel = new QLabel(sText, pParent); pLabel->setAlignment(Qt::AlignCenter); QPalette oPalette = pLabel->palette(); oPalette.setColor(QPalette::WindowText, pParent->palette().color(QPalette::Mid)); pLabel->setPalette(oPalette); return pLabel; } QString displayValueText(double dValue, int nDigit) { if (nDigit <= 0) { return QString::number(dValue, 'f', 0); } return ZxBaseUtil::getValidStr(dValue, nDigit); } // 显示相下拉框只列出实际出现过非零流量的相;多相表中的固定全零列 // 仍保留在井记录和求解输入中,但不应显示成该井存在对应相产量。 bool hasRealPhaseData(const QVector& vecPoints) { int nStartIndex = 0; if (!vecPoints.isEmpty() && vecPoints.first().x() == 0.0 && vecPoints.first().y() == 0.0) { nStartIndex = 1; } for (int nIndex = nStartIndex; nIndex < vecPoints.size(); ++nIndex) { if (vecPoints[nIndex].y() != 0.0) { return true; } } return false; } // 压力/流量记录下拉框只显示稳定的 Gauge Code,避免顶部工具栏过宽。 // 异常旧数据没有 Code 时,才回退到框架名称。 QString formatGaugeRecordLabel(const QString& sGaugeName, const QString& sGaugeCode, const QString& sGaugeTime) { Q_UNUSED(sGaugeTime); return sGaugeCode.isEmpty() ? sGaugeName : sGaugeCode; } QString gaugeMetadataText(const QString& sText) { return sText.trimmed().isEmpty() ? nmWellPressureFlowPage::tr("(none)") : sText; } QString gaugeNameText(const QString& sGaugeName) { return sGaugeName.trimmed().isEmpty() ? nmWellPressureFlowPage::tr("(unnamed)") : sGaugeName; } QString gaugeRecordToolTip(const QString& sGaugeName, const QString& sGaugeCode, const QString& sGaugeTime) { QStringList listLines; listLines << nmWellPressureFlowPage::tr("Curve name: %1") .arg(gaugeNameText(sGaugeName)); listLines << nmWellPressureFlowPage::tr("Gauge Code: %1") .arg(gaugeMetadataText(sGaugeCode)); listLines << nmWellPressureFlowPage::tr("Measurement time: %1") .arg(gaugeMetadataText(sGaugeTime)); return listLines.join(QLatin1String("\n")); } void disableGaugeComboItem(QComboBox* pCombo, int nItemIndex) { QStandardItemModel* pModel = qobject_cast(pCombo->model()); if (pModel == nullptr) { return; } QStandardItem* pItem = pModel->item(nItemIndex); if (pItem != nullptr) { pItem->setFlags(pItem->flags() & ~Qt::ItemIsEnabled); } } void updatePreviewComboToolTip(QComboBox* pCombo) { if (pCombo == nullptr || pCombo->currentIndex() < 0 || pCombo->itemData(pCombo->currentIndex()).toString().isEmpty()) { if (pCombo != nullptr) { pCombo->setToolTip(QString()); } return; } QString sToolTip = pCombo->itemData( pCombo->currentIndex(), Qt::ToolTipRole).toString(); if (!sToolTip.isEmpty()) { sToolTip += QLatin1Char('\n'); } sToolTip += nmWellPressureFlowPage::tr( "Preview only; calculation settings are unchanged."); pCombo->setToolTip(sToolTip); } // 统一向压力/流量记录下拉框追加一项;异常记录保留状态提示但不可选。 void appendGaugeComboItem(QComboBox* pCombo, const QString& sGaugeName, const QString& sGaugeCode, const QString& sGaugeTime, NM_GAUGE_RECORD_STATUS eStatus) { QString sLabel = formatGaugeRecordLabel(sGaugeName, sGaugeCode, sGaugeTime); switch (eStatus) { case NM_GaugeRecord_InvalidIdentity: sLabel += nmWellPressureFlowPage::tr(" (Invalid)"); break; case NM_GaugeRecord_Empty: sLabel += nmWellPressureFlowPage::tr(" (Empty)"); break; case NM_GaugeRecord_Corrupt: sLabel += nmWellPressureFlowPage::tr(" (Corrupt)"); break; default: break; } pCombo->addItem(sLabel, sGaugeCode); const int nItemIndex = pCombo->count() - 1; pCombo->setItemData(nItemIndex, gaugeRecordToolTip(sGaugeName, sGaugeCode, sGaugeTime), Qt::ToolTipRole); if (eStatus != NM_GaugeRecord_Usable) { disableGaugeComboItem(pCombo, nItemIndex); } } // 诊断文本统一使用约 15 位有效数字,避免浮点差异被显示精度掩盖。 QString diagNumberText(double dValue) { return QString::number(dValue, 'g', 15); } // 诊断文本中的单个数据点,括号与逗号为固定格式不参与翻译。 QString diagPointText(const QPointF& oPoint) { return QString("(%1, %2)") .arg(diagNumberText(oPoint.x())) .arg(diagNumberText(oPoint.y())); } // 空字符串在诊断文本中显示为“(none)”,避免与真实空白混淆。 QString diagTextOrNone(const QString& sText) { return sText.isEmpty() ? nmWellPressureFlowPage::tr("(none)") : sText; } // 记录状态的诊断文本,与下拉框后缀语义一致,单独成词便于阅读。 QString diagStatusText(NM_GAUGE_RECORD_STATUS eStatus) { switch (eStatus) { case NM_GaugeRecord_Usable: return nmWellPressureFlowPage::tr("Usable"); case NM_GaugeRecord_Empty: return nmWellPressureFlowPage::tr("Empty"); case NM_GaugeRecord_Corrupt: return nmWellPressureFlowPage::tr("Corrupt"); case NM_GaugeRecord_InvalidIdentity: return nmWellPressureFlowPage::tr("Invalid identity"); default: return nmWellPressureFlowPage::tr("Unknown"); } } // 井别的诊断文本。 QString diagWellCategoryText(NM_WELL_CATEGORY eWellCategory) { switch (eWellCategory) { case NM_WellCategory_Oil: return nmWellPressureFlowPage::tr("Oil well"); case NM_WellCategory_Gas: return nmWellPressureFlowPage::tr("Gas well"); case NM_WellCategory_Water: return nmWellPressureFlowPage::tr("Water well"); default: return nmWellPressureFlowPage::tr("Unknown"); } } // 相名的诊断文本,与显示相下拉框文本保持一致。 QString diagPhaseText(NM_PHASE_TYPE ePhase) { switch (ePhase) { case PHASE_Oil: return nmWellPressureFlowPage::tr("Oil"); case PHASE_Gas: return nmWellPressureFlowPage::tr("Gas"); case PHASE_Water: return nmWellPressureFlowPage::tr("Water"); default: return nmWellPressureFlowPage::tr("Unknown"); } } // 当前分析窗口的PVT相态决定实际参与分析的相,不能仅依赖多相布尔标志判断。 QString diagFluidTypeText(PvtFluidType eFluidType) { switch (eFluidType) { case WFT_Oil: return nmWellPressureFlowPage::tr("Oil"); case WFT_Gas: return nmWellPressureFlowPage::tr("Gas"); case WFT_Water: return nmWellPressureFlowPage::tr("Water"); case WFT_Oil_Gas: return nmWellPressureFlowPage::tr("Oil + Gas"); case WFT_Oil_Water: return nmWellPressureFlowPage::tr("Oil + Water"); case WFT_Gas_Water: return nmWellPressureFlowPage::tr("Gas + Water"); case WTF_Oil_Gas_Water: return nmWellPressureFlowPage::tr("Oil + Gas + Water"); case WFT_Null: return nmWellPressureFlowPage::tr("None"); default: return nmWellPressureFlowPage::tr("Other (%1)") .arg(static_cast(eFluidType)); } } // 判断首个点是否为 (0,0) 占位点,与 hasRealPhaseData 的占位点语义一致。 bool hasLeadingPlaceholderPoint(const QVector& vecPoints) { return !vecPoints.isEmpty() && vecPoints.first().x() == 0.0 && vecPoints.first().y() == 0.0; } // 去除可选的首个 (0,0) 占位点后返回真实流量段列表。 QVector realPhaseSegments(const QVector& vecPoints) { QVector vecSegments = vecPoints; if (hasLeadingPlaceholderPoint(vecSegments)) { vecSegments.remove(0); } return vecSegments; } // 生成单个相的诊断摘要行:原始点数、真实段数、累计时长、首末点与占位点标识。 void appendPhaseDiagLines(QStringList& listLines, const QString& sPhaseName, const QVector& vecPoints) { if (vecPoints.isEmpty()) { listLines << QString(" %1: %2") .arg(sPhaseName) .arg(nmWellPressureFlowPage::tr("no data")); return; } // 累计时长只统计真实段,流量点 X 值是段持续时间而非绝对时刻。 QVector vecSegments = realPhaseSegments(vecPoints); double dTotalDuration = 0.0; for (int nIndex = 0; nIndex < vecSegments.size(); ++nIndex) { dTotalDuration += vecSegments[nIndex].x(); } listLines << QString(" %1: %2") .arg(sPhaseName) .arg(nmWellPressureFlowPage::tr( "raw points = %1, real segments = %2, total duration = %3") .arg(vecPoints.size()) .arg(vecSegments.size()) .arg(diagNumberText(dTotalDuration))); listLines << QLatin1String(" ") + nmWellPressureFlowPage::tr("First point = %1, last point = %2") .arg(diagPointText(vecPoints.first())) .arg(diagPointText(vecPoints.last())); // 点数较少时列出全部原始点,便于核对各相中间点数值是否分相正确。 const int nMaxListedPoints = 16; if (vecPoints.size() <= nMaxListedPoints) { QStringList listPointTexts; for (int nIndex = 0; nIndex < vecPoints.size(); ++nIndex) { listPointTexts << diagPointText(vecPoints[nIndex]); } listLines << QLatin1String(" ") + nmWellPressureFlowPage::tr("All points = %1") .arg(listPointTexts.join(QLatin1String(", "))); } if (hasLeadingPlaceholderPoint(vecPoints)) { listLines << QLatin1String(" ") + nmWellPressureFlowPage::tr("first point is a (0,0) placeholder"); } } // 检查多相记录有效相之间段数与每段时间是否完全一致,返回结论文本。 QString diagPhaseConsistencyText(const nmFlowGaugeRecord& oRecord) { // 只统计有真实数据的相,与显示相下拉框的判定保持一致;比较前先去除占位点。 QStringList listPhaseNames; QVector > listPhaseSegments; if (hasRealPhaseData(oRecord.vecOilPoints)) { listPhaseNames.append(nmWellPressureFlowPage::tr("Oil")); listPhaseSegments.append(realPhaseSegments(oRecord.vecOilPoints)); } if (hasRealPhaseData(oRecord.vecGasPoints)) { listPhaseNames.append(nmWellPressureFlowPage::tr("Gas")); listPhaseSegments.append(realPhaseSegments(oRecord.vecGasPoints)); } if (hasRealPhaseData(oRecord.vecWaterPoints)) { listPhaseNames.append(nmWellPressureFlowPage::tr("Water")); listPhaseSegments.append(realPhaseSegments(oRecord.vecWaterPoints)); } if (listPhaseSegments.size() < 2) { return nmWellPressureFlowPage::tr( "less than two phases with real data, no comparison"); } // 先比较段数,段数不同时直接列出各相段数。 for (int nPhase = 1; nPhase < listPhaseSegments.size(); ++nPhase) { if (listPhaseSegments[nPhase].size() != listPhaseSegments.first().size()) { QStringList listCounts; for (int nItem = 0; nItem < listPhaseSegments.size(); ++nItem) { listCounts << QString("%1 = %2") .arg(listPhaseNames[nItem]) .arg(listPhaseSegments[nItem].size()); } return nmWellPressureFlowPage::tr("segment counts differ: %1") .arg(listCounts.join(QLatin1String(", "))); } } // 段数一致后逐段严格按位比较时间,不引入容差,浮点差异按不一致处理。 for (int nSegment = 0; nSegment < listPhaseSegments.first().size(); ++nSegment) { for (int nPhase = 1; nPhase < listPhaseSegments.size(); ++nPhase) { if (listPhaseSegments[nPhase][nSegment].x() != listPhaseSegments.first()[nSegment].x()) { return nmWellPressureFlowPage::tr( "segment counts equal (%1), but segment %2 time differs: %3 = %4, %5 = %6") .arg(listPhaseSegments.first().size()) .arg(nSegment + 1) .arg(listPhaseNames.first()) .arg(diagNumberText( listPhaseSegments.first()[nSegment].x())) .arg(listPhaseNames[nPhase]) .arg(diagNumberText( listPhaseSegments[nPhase][nSegment].x())); } } } return nmWellPressureFlowPage::tr( "segment counts equal (%1) and all segment times identical") .arg(listPhaseSegments.first().size()); } // 输出一个原始字节块按 VVecVariant 解码后的形状和少量样本值。 // 诊断只读取数据,不参与正式的流量记录状态判定。 void appendRawVVecDiagnostic(QStringList& listLines, const QString& sBlockName, const QByteArray& baSource) { listLines << QString(" %1 bytes: %2") .arg(sBlockName) .arg(baSource.size()); if (baSource.isEmpty()) { listLines << QString(" %1 is empty").arg(sBlockName); return; } QByteArray baData = baSource; VVecVariant vvecData; const bool bDecoded = ZxBaHelper::convertBa2VVec(vvecData, baData); listLines << QString(" VVec decode: %1") .arg(bDecoded ? QLatin1String("success") : QLatin1String("failed")); if (!bDecoded) { return; } QStringList listSizes; for (int nVectorIndex = 0; nVectorIndex < vvecData.size(); ++nVectorIndex) { listSizes << QString("[%1]=%2") .arg(nVectorIndex) .arg(vvecData[nVectorIndex].size()); } listLines << QString(" Vector count: %1; sizes: %2") .arg(vvecData.size()) .arg(listSizes.isEmpty() ? QLatin1String("(none)") : listSizes.join(QLatin1String(", "))); // GaugeDataEx2 在框架中的正式布局是 N 行 x 4 列,直接按业务字段展示。 if (sBlockName == QLatin1String("GaugeDataEx2")) { bool bRowLayoutValid = !vvecData.isEmpty(); for (int nStoredRow = 0; bRowLayoutValid && nStoredRow < vvecData.size(); ++nStoredRow) { bRowLayoutValid = vvecData[nStoredRow].size() == 4; } listLines << QString(" N x 4 row layout valid: %1") .arg(bRowLayoutValid ? QLatin1String("Yes") : QLatin1String("No")); if (bRowLayoutValid) { const int nMaxStoredRows = 8; const int nStoredRowCount = qMin( vvecData.size(), nMaxStoredRows); for (int nStoredRow = 0; nStoredRow < nStoredRowCount; ++nStoredRow) { listLines << QString(" Stored row %1: duration=%2, oil=%3, gas=%4, water=%5") .arg(nStoredRow + 1) .arg(vvecData[nStoredRow][0].toString()) .arg(vvecData[nStoredRow][1].toString()) .arg(vvecData[nStoredRow][2].toString()) .arg(vvecData[nStoredRow][3].toString()); } return; } } // 其他未知形状的扩展块按矩阵公共范围输出样本,便于继续排查。 const int nMaxSampleRows = 8; int nCommonSize = vvecData.isEmpty() ? 0 : vvecData.first().size(); for (int nVectorIndex = 1; nVectorIndex < vvecData.size(); ++nVectorIndex) { nCommonSize = qMin(nCommonSize, vvecData[nVectorIndex].size()); } const int nSampleRows = qMin(nCommonSize, nMaxSampleRows); for (int nRowIndex = 0; nRowIndex < nSampleRows; ++nRowIndex) { QStringList listValues; for (int nVectorIndex = 0; nVectorIndex < vvecData.size(); ++nVectorIndex) { listValues << QString("[%1]=%2") .arg(nVectorIndex) .arg(vvecData[nVectorIndex][nRowIndex].toString()); } listLines << QString(" Row %1: %2") .arg(nRowIndex + 1) .arg(listValues.join(QLatin1String(", "))); } } // 直接定位当前数值井对应的框架井,检查每条流量记录的三个存储块及 // getGaugeDataOf() 返回值,区分“数值层解析错误”和“框架原始数据形状异常”。 void appendFrameworkFlowStorageDiagnostic( QStringList& listLines, const QString& sWellCode, const QString& sSelectedFlowGaugeCode) { listLines << QString(); listLines << QLatin1String("=== Framework raw flow storage ==="); listLines << QString("Requested well code: %1") .arg(sWellCode.isEmpty() ? QLatin1String("(none)") : sWellCode); listLines << QString("Selected flow gauge code: %1") .arg(sSelectedFlowGaugeCode.isEmpty() ? QLatin1String("(none)") : sSelectedFlowGaugeCode); if (zxCurProject == nullptr) { listLines << QLatin1String("Current framework project: unavailable"); return; } const ZxDataObjectList listWells = zxCurProject->getChildren(iDataModelType::sTypeWell); ZxDataWell* pMatchedWell = nullptr; int nMatchedWellCount = 0; for (int nWellIndex = 0; nWellIndex < listWells.size(); ++nWellIndex) { ZxDataWell* pFrameworkWell = dynamic_cast(listWells[nWellIndex]); if (pFrameworkWell != nullptr && pFrameworkWell->getCode() == sWellCode) { ++nMatchedWellCount; if (pMatchedWell == nullptr) { pMatchedWell = pFrameworkWell; } } } listLines << QString("Framework wells matched by code: %1") .arg(nMatchedWellCount); if (pMatchedWell == nullptr) { return; } listLines << QString("Matched framework well name: %1") .arg(pMatchedWell->getName()); listLines << QString("Framework well type index: %1") .arg(pMatchedWell->getWellTypeIndex()); const ZxDataObjectList listGaugeF = pMatchedWell->getChildren( iDataModelType::sTypeDataGaugeF); listLines << QString("Framework flow-record count: %1") .arg(listGaugeF.size()); bool bSelectedGaugeFound = false; for (int nGaugeIndex = 0; nGaugeIndex < listGaugeF.size(); ++nGaugeIndex) { ZxDataGaugeF* pGaugeF = dynamic_cast(listGaugeF[nGaugeIndex]); if (pGaugeF == nullptr) { listLines << QString("[%1] Object is not ZxDataGaugeF") .arg(nGaugeIndex + 1); continue; } const bool bIsSelected = pGaugeF->getCode() == sSelectedFlowGaugeCode; bSelectedGaugeFound = bSelectedGaugeFound || bIsSelected; const QByteArray baGaugeData = pGaugeF->getGaugeData(); const QByteArray baGaugeDataEx2 = pGaugeF->getGaugeDataEx2(); const QByteArray baGaugeDataEx3 = pGaugeF->getGaugeDataEx3(); const QByteArray baDataOf0 = pGaugeF->getGaugeDataOf(0); const QByteArray baDataOf1 = pGaugeF->getGaugeDataOf(1); const QByteArray baDataOf2 = pGaugeF->getGaugeDataOf(2); listLines << QString(); listLines << QString("[%1] Code: %2; name: %3; selected: %4") .arg(nGaugeIndex + 1) .arg(pGaugeF->getCode()) .arg(pGaugeF->getGaugeName()) .arg(bIsSelected ? QLatin1String("Yes") : QLatin1String("No")); listLines << QString(" Time: %1; multiphase flag: %2") .arg(pGaugeF->getGaugeTime()) .arg(pGaugeF->getMultiPhase() ? QLatin1String("Yes") : QLatin1String("No")); // 主块按单相 XY 格式再解一次,确认单相数据是否仍然存在。 QByteArray baMainCopy = baGaugeData; VecDouble vecMainX; VecDouble vecMainY; const bool bMainDecoded = !baGaugeData.isEmpty() && ZxBaHelper::convertBa2VecXY( vecMainX, vecMainY, baMainCopy); listLines << QString(" GaugeData bytes: %1; XY decode: %2; X/Y sizes: %3/%4") .arg(baGaugeData.size()) .arg(bMainDecoded ? QLatin1String("success") : QLatin1String("failed")) .arg(static_cast(vecMainX.size())) .arg(static_cast(vecMainY.size())); appendRawVVecDiagnostic( listLines, QLatin1String("GaugeDataEx2"), baGaugeDataEx2); appendRawVVecDiagnostic( listLines, QLatin1String("GaugeDataEx3"), baGaugeDataEx3); listLines << QString(" getGaugeDataOf sizes: [0]=%1, [1]=%2, [2]=%3") .arg(baDataOf0.size()) .arg(baDataOf1.size()) .arg(baDataOf2.size()); listLines << QString(" getGaugeDataOf equality: [0]=GaugeData %1, [1]=Ex2 %2, [2]=Ex3 %3") .arg(baDataOf0 == baGaugeData ? QLatin1String("Yes") : QLatin1String("No")) .arg(baDataOf1 == baGaugeDataEx2 ? QLatin1String("Yes") : QLatin1String("No")) .arg(baDataOf2 == baGaugeDataEx3 ? QLatin1String("Yes") : QLatin1String("No")); } listLines << QString(); listLines << QString("Selected gauge found under matched framework well: %1") .arg(bSelectedGaugeFound ? QLatin1String("Yes") : QLatin1String("No")); } } nmWellPressureFlowPage::nmWellPressureFlowPage(QWidget* pParent) : QWidget(pParent), m_pWell(nullptr), m_pContentStack(nullptr), m_pHorizontalSplitter(nullptr), m_pChartSplitter(nullptr), m_pPressureChartStack(nullptr), m_pFlowChartStack(nullptr), m_pFlowTableStack(nullptr), m_pPressureChart(nullptr), m_pFlowChart(nullptr), m_pFlowTable(nullptr), m_pFlowHeader(nullptr), m_pTimeUnitGroup(nullptr), m_pFlowUnitGroup(nullptr), m_sTimeBaseUnit(preferredTimeUnit()), m_sFlowBaseUnit(preferredFlowUnit()), m_sTimeUnit(preferredTimeUnit()), m_sFlowUnit(preferredFlowUnit()), m_nTimeDigit(6), m_nFlowDigit(6), m_nSelectedFlowSegment(-1), m_pRecordSelectionBar(nullptr), m_pPressureGaugeCombo(nullptr), m_pFlowGaugeCombo(nullptr), m_pDisplayPhaseCombo(nullptr), m_pDisplayPhaseContainer(nullptr), m_pCheckDataButton(nullptr), m_eDisplayPhase(PHASE_UNKNOWN) { createUi(); initializeUnitSupport(); resetDataViews(); } void nmWellPressureFlowPage::setWell(nmDataWellBase* pWell) { if (m_pWell == pWell) { return; } m_pWell = pWell; m_sPreviewPressureGaugeCode = pWell == nullptr ? QString() : pWell->getSelectedPressureGaugeCode(); m_sPreviewFlowGaugeCode = pWell == nullptr ? QString() : pWell->getSelectedFlowGaugeCode(); resetDataViews(); } void nmWellPressureFlowPage::refreshData() { if (m_pWell == nullptr) { resetDataViews(); return; } // 第一步:恢复页面自己的预览记录,不修改井对象保存的计算记录选择。 populatePressureGaugeCombo(); populateFlowGaugeCombo(); populateDisplayPhaseCombo(currentFlowRecord()); // 第二步:直接从预览记录复制数据,后续记录、相别和单位切换都不回写井对象。 const nmPressureGaugeRecord* pPressureRecord = currentPressureRecord(); const nmFlowGaugeRecord* pFlowRecord = currentFlowRecord(); QVector vecRawPressurePoints = pPressureRecord == nullptr ? QVector() : pPressureRecord->vecPressurePoints; QVector vecRawFlowPoints; if (pFlowRecord != nullptr) { switch (m_eDisplayPhase) { case PHASE_Oil: vecRawFlowPoints = pFlowRecord->vecOilPoints; break; case PHASE_Gas: vecRawFlowPoints = pFlowRecord->vecGasPoints; break; case PHASE_Water: vecRawFlowPoints = pFlowRecord->vecWaterPoints; break; default: break; } } // 第三步:单位服务可能晚于页面构造完成,再刷新一次可用单位组。 initializeUnitSupport(); m_vecRawPressurePoints = vecRawPressurePoints; m_vecRawFlowPoints = vecRawFlowPoints; QVector vecTableFlowPoints; int nTimeDigit = m_nTimeDigit; int nFlowDigit = m_nFlowDigit; if (!buildFlowTableData(m_sTimeUnit, m_sFlowUnit, vecTableFlowPoints, nTimeDigit, nFlowDigit)) { // 当前显示单位不可用时回退基准单位,禁止继续展示旧井快照。 m_sTimeUnit = m_sTimeBaseUnit; m_sFlowUnit = m_sFlowBaseUnit; if (!buildFlowTableData(m_sTimeUnit, m_sFlowUnit, vecTableFlowPoints, nTimeDigit, nFlowDigit)) { resetDataViews(); updateUnitHeader(); return; } } // 第三步:换算与精度读取全部成功后再提交,曲线始终读取原始数据。 m_vecTableFlowPoints = vecTableFlowPoints; m_nTimeDigit = nTimeDigit; m_nFlowDigit = nFlowDigit; updateUnitHeader(); updateViews(); } void nmWellPressureFlowPage::clearData() { m_pWell = nullptr; m_sPreviewPressureGaugeCode.clear(); m_sPreviewFlowGaugeCode.clear(); resetDataViews(); } void nmWellPressureFlowPage::slotUnitChanged( int nColumn, const iUnitItem* pSourceUnit, const iUnitItem* pDestinationUnit) { Q_UNUSED(pSourceUnit); if (pDestinationUnit == nullptr || (nColumn != 0 && nColumn != 1)) { updateUnitHeader(); return; } QString sTimeUnit = m_sTimeUnit; QString sFlowUnit = m_sFlowUnit; if (nColumn == 0) { if (m_pTimeUnitGroup == nullptr) { updateUnitHeader(); return; } sTimeUnit = pDestinationUnit->m_sName; } else { if (m_pFlowUnitGroup == nullptr) { updateUnitHeader(); return; } sFlowUnit = pDestinationUnit->m_sName; } // 每次都从基准副本换算,禁止使用当前显示值继续换算。 QVector vecTableFlowPoints; int nTimeDigit = m_nTimeDigit; int nFlowDigit = m_nFlowDigit; if (!buildFlowTableData(sTimeUnit, sFlowUnit, vecTableFlowPoints, nTimeDigit, nFlowDigit)) { updateUnitHeader(); return; } m_sTimeUnit = sTimeUnit; m_sFlowUnit = sFlowUnit; m_vecTableFlowPoints = vecTableFlowPoints; m_nTimeDigit = nTimeDigit; m_nFlowDigit = nFlowDigit; updateUnitHeader(); updateFlowTable(); } void nmWellPressureFlowPage::slotFlowRowChanged( int nCurrentRow, int nCurrentColumn, int nPreviousRow, int nPreviousColumn) { Q_UNUSED(nCurrentColumn); Q_UNUSED(nPreviousRow); Q_UNUSED(nPreviousColumn); applyFlowSegmentSelection(nCurrentRow); } void nmWellPressureFlowPage::slotFlowChartPointClicked(double dTimePoint) { // 曲线保持基准单位,点击时间先换算为表格当前单位再定位行。 double dTableTimePoint = 0.0; if (!convertValue(m_pTimeUnitGroup, m_sTimeBaseUnit, m_sTimeUnit, dTimePoint, dTableTimePoint)) { applyFlowSegmentSelection(-1); return; } applyFlowSegmentSelection( findFlowSegmentByTableTime(dTableTimePoint)); } void nmWellPressureFlowPage::slotPressureGaugePreviewChanged(int nIndex) { if (m_pWell == nullptr || nIndex < 0) { return; } const QString sGaugeCode = m_pPressureGaugeCombo->itemData(nIndex).toString(); if (sGaugeCode.isEmpty() || sGaugeCode == m_sPreviewPressureGaugeCode) { updatePreviewComboToolTip(m_pPressureGaugeCombo); return; } m_sPreviewPressureGaugeCode = sGaugeCode; refreshData(); } void nmWellPressureFlowPage::slotFlowGaugePreviewChanged(int nIndex) { if (m_pWell == nullptr || nIndex < 0) { return; } const QString sGaugeCode = m_pFlowGaugeCombo->itemData(nIndex).toString(); if (sGaugeCode.isEmpty() || sGaugeCode == m_sPreviewFlowGaugeCode) { updatePreviewComboToolTip(m_pFlowGaugeCombo); return; } m_sPreviewFlowGaugeCode = sGaugeCode; m_eDisplayPhase = defaultDisplayPhase(currentFlowRecord()); m_nSelectedFlowSegment = -1; refreshData(); } void nmWellPressureFlowPage::slotDisplayPhaseChanged(int nIndex) { if (m_pWell == nullptr || nIndex < 0) { return; } NM_PHASE_TYPE eNewPhase = static_cast( m_pDisplayPhaseCombo->itemData(nIndex).toInt()); if (eNewPhase == m_eDisplayPhase) { return; } // 仅重新按新相取点刷新视图,不改变记录内容、井别、m_nIndexF 或求解器输入。 m_eDisplayPhase = eNewPhase; refreshData(); } void nmWellPressureFlowPage::slotCheckDataClicked() { // 诊断窗口为模态只读展示,允许缩放、滚动与复制文本,不修改任何业务数据。 QDialog oDialog(this); oDialog.setWindowTitle(tr("Pressure/Flow Data Check")); oDialog.setWindowFlags(oDialog.windowFlags() | Qt::WindowMaximizeButtonHint); oDialog.resize(820, 600); QVBoxLayout* pDialogLayout = new QVBoxLayout(&oDialog); QPlainTextEdit* pTextEdit = new QPlainTextEdit(&oDialog); pTextEdit->setReadOnly(true); pTextEdit->setLineWrapMode(QPlainTextEdit::NoWrap); pTextEdit->setPlainText(buildDiagnosticText()); QPushButton* pCloseButton = new QPushButton(tr("Close"), &oDialog); QHBoxLayout* pButtonLayout = new QHBoxLayout(); pButtonLayout->addStretch(1); pButtonLayout->addWidget(pCloseButton); pDialogLayout->addWidget(pTextEdit, 1); pDialogLayout->addLayout(pButtonLayout); connect(pCloseButton, SIGNAL(clicked()), &oDialog, SLOT(accept())); oDialog.exec(); } void nmWellPressureFlowPage::createUi() { QVBoxLayout* pMainLayout = new QVBoxLayout(this); pMainLayout->setContentsMargins(8, 6, 8, 8); pMainLayout->setSpacing(0); m_pContentStack = new QStackedWidget(this); m_pHorizontalSplitter = new QSplitter( Qt::Horizontal, m_pContentStack); m_pHorizontalSplitter->setChildrenCollapsible(false); // 左侧上下两图共享时间范围,但各自保留独立空状态。 m_pChartSplitter = new QSplitter( Qt::Vertical, m_pHorizontalSplitter); m_pChartSplitter->setChildrenCollapsible(false); m_pPressureChartStack = new QStackedWidget(m_pChartSplitter); m_pPressureChart = new nmWxPressFlowChartWidget( m_pPressureChartStack); m_pPressureChart->setReadOnly(true); m_pPressureChart->setTimeSelectionEnabled(false); m_pPressureChart->setAxisLabels(tr("Time"), tr("Pressure")); m_pPressureChart->setAxisUnits( m_sTimeBaseUnit, tr("MPa")); m_pPressureChart->setLineColor(QColor(50, 200, 50)); m_pPressureChart->setChartType(CHART_TYPE_LINE); m_pPressureChartStack->addWidget(m_pPressureChart); m_pPressureChartStack->addWidget(createEmptyLabel( tr("No pressure data"), m_pPressureChartStack)); m_pFlowChartStack = new QStackedWidget(m_pChartSplitter); m_pFlowChart = new nmWxPressFlowChartWidget(m_pFlowChartStack); m_pFlowChart->setReadOnly(true); m_pFlowChart->setTimeSelectionEnabled(true); m_pFlowChart->setAxisLabels(tr("Time"), tr("Flow")); m_pFlowChart->setAxisUnits(m_sTimeBaseUnit, m_sFlowBaseUnit); m_pFlowChart->setLineColor(QColor(255, 0, 0)); m_pFlowChart->setChartType(CHART_TYPE_STEP); m_pFlowChartStack->addWidget(m_pFlowChart); m_pFlowChartStack->addWidget(createEmptyLabel( tr("No flow data"), m_pFlowChartStack)); m_pChartSplitter->addWidget(m_pPressureChartStack); m_pChartSplitter->addWidget(m_pFlowChartStack); m_pChartSplitter->setStretchFactor(0, 1); m_pChartSplitter->setStretchFactor(1, 1); // 右侧仅显示原始流量段对应的时间和流量,不提供任何编辑入口。 m_pFlowTableStack = new QStackedWidget(m_pHorizontalSplitter); m_pFlowTable = new QTableWidget(m_pFlowTableStack); m_pFlowTable->setColumnCount(2); m_pFlowTable->setEditTriggers(QAbstractItemView::NoEditTriggers); m_pFlowTable->setSelectionBehavior(QAbstractItemView::SelectRows); m_pFlowTable->setSelectionMode(QAbstractItemView::SingleSelection); m_pFlowTable->setSortingEnabled(false); m_pFlowTable->setDragEnabled(false); m_pFlowTable->setAcceptDrops(false); m_pFlowTable->setDropIndicatorShown(false); m_pFlowTable->setAlternatingRowColors(true); m_pFlowTable->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_pFlowHeader = new ZxTableHeaderViewUnit(m_pFlowTable); m_pFlowTable->setHorizontalHeader(m_pFlowHeader); m_pFlowTable->horizontalHeader()->setMovable(false); m_pFlowTable->horizontalHeader()->setResizeMode(QHeaderView::Stretch); m_pFlowTable->verticalHeader()->setMovable(false); m_pFlowTable->verticalHeader()->setDefaultAlignment(Qt::AlignCenter); m_pFlowTable->verticalHeader()->setMinimumWidth(18); m_pFlowTableStack->addWidget(m_pFlowTable); m_pFlowTableStack->addWidget(createEmptyLabel( tr("No flow data"), m_pFlowTableStack)); m_pHorizontalSplitter->addWidget(m_pChartSplitter); m_pHorizontalSplitter->addWidget(m_pFlowTableStack); m_pHorizontalSplitter->setStretchFactor(0, 7); m_pHorizontalSplitter->setStretchFactor(1, 3); QList listHorizontalSizes; listHorizontalSizes << 700 << 300; m_pHorizontalSplitter->setSizes(listHorizontalSizes); m_pContentStack->addWidget(m_pHorizontalSplitter); m_pContentStack->addWidget(createEmptyLabel( tr("No pressure or flow data"), m_pContentStack)); // 顶部插入压力/流量记录与显示相三个下拉框所在行,不改变下方图表/表格结构。 createRecordSelectionBar(); pMainLayout->addWidget(m_pRecordSelectionBar); pMainLayout->addWidget(m_pContentStack, 1); connect(m_pFlowHeader, SIGNAL(sigCbxValueChanged(int,const iUnitItem*,const iUnitItem*)), this, SLOT(slotUnitChanged(int,const iUnitItem*,const iUnitItem*))); connect(m_pFlowTable, SIGNAL(currentCellChanged(int,int,int,int)), this, SLOT(slotFlowRowChanged(int,int,int,int))); connect(m_pFlowChart, SIGNAL(chartPointClicked(double)), this, SLOT(slotFlowChartPointClicked(double))); } void nmWellPressureFlowPage::createRecordSelectionBar() { m_pRecordSelectionBar = new QWidget(this); QHBoxLayout* pBarLayout = new QHBoxLayout(m_pRecordSelectionBar); pBarLayout->setContentsMargins(0, 0, 0, 6); pBarLayout->setSpacing(6); QLabel* pPressureLabel = new QLabel( tr("Pressure Record"), m_pRecordSelectionBar); m_pPressureGaugeCombo = new QComboBox(m_pRecordSelectionBar); m_pPressureGaugeCombo->setEnabled(false); m_pPressureGaugeCombo->setMinimumWidth(160); m_pPressureGaugeCombo->setMaxVisibleItems(16); 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->setMaxVisibleItems(16); // 显示相是纯展示状态,整体容器随记录是否为多相含数据而隐藏/显示。 m_pDisplayPhaseContainer = new QWidget(m_pRecordSelectionBar); QHBoxLayout* pPhaseLayout = new QHBoxLayout(m_pDisplayPhaseContainer); pPhaseLayout->setContentsMargins(0, 0, 0, 0); pPhaseLayout->setSpacing(6); QLabel* pPhaseLabel = new QLabel( tr("Display Phase"), m_pDisplayPhaseContainer); m_pDisplayPhaseCombo = new QComboBox(m_pDisplayPhaseContainer); m_pDisplayPhaseCombo->setMinimumWidth(80); pPhaseLayout->addWidget(pPhaseLabel); pPhaseLayout->addWidget(m_pDisplayPhaseCombo); m_pDisplayPhaseContainer->setVisible(false); // 临时诊断按钮:只读展示井对象解析结果,验证完成后再决定删除或保留。 m_pCheckDataButton = new QPushButton( tr("Check Data"), m_pRecordSelectionBar); pBarLayout->addWidget(pPressureLabel); pBarLayout->addWidget(m_pPressureGaugeCombo); pBarLayout->addSpacing(12); pBarLayout->addWidget(pFlowLabel); pBarLayout->addWidget(m_pFlowGaugeCombo); pBarLayout->addSpacing(12); pBarLayout->addWidget(m_pDisplayPhaseContainer); pBarLayout->addStretch(1); pBarLayout->addWidget(m_pCheckDataButton); connect(m_pPressureGaugeCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPressureGaugePreviewChanged(int))); connect(m_pFlowGaugeCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotFlowGaugePreviewChanged(int))); connect(m_pDisplayPhaseCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotDisplayPhaseChanged(int))); connect(m_pCheckDataButton, SIGNAL(clicked()), this, SLOT(slotCheckDataClicked())); } void nmWellPressureFlowPage::populatePressureGaugeCombo() { if (m_pPressureGaugeCombo == nullptr) { return; } bool bSignalsWereBlocked = m_pPressureGaugeCombo->blockSignals(true); m_pPressureGaugeCombo->clear(); if (m_pWell == nullptr) { m_pPressureGaugeCombo->setEnabled(false); m_pPressureGaugeCombo->blockSignals(bSignalsWereBlocked); return; } m_vecPressureGaugeRecords = m_pWell->getPressureRecords(); int nPreviewIndex = -1; int nCalculationIndex = -1; int nFirstUsableIndex = -1; const QString sCalculationCode = m_pWell->getSelectedPressureGaugeCode(); for (int nIndex = 0; nIndex < m_vecPressureGaugeRecords.size(); ++nIndex) { const nmPressureGaugeRecord& oRecord = m_vecPressureGaugeRecords[nIndex]; appendGaugeComboItem(m_pPressureGaugeCombo, oRecord.sGaugeName, oRecord.sGaugeCode, oRecord.sGaugeTime, oRecord.eStatus); const int nItemIndex = m_pPressureGaugeCombo->count() - 1; if (oRecord.eStatus != NM_GaugeRecord_Usable) { continue; } if (nFirstUsableIndex < 0) { nFirstUsableIndex = nItemIndex; } if (oRecord.sGaugeCode == m_sPreviewPressureGaugeCode) { nPreviewIndex = nItemIndex; } if (oRecord.sGaugeCode == sCalculationCode) { nCalculationIndex = nItemIndex; } } const int nCurrentIndex = nPreviewIndex >= 0 ? nPreviewIndex : (nCalculationIndex >= 0 ? nCalculationIndex : (nFirstUsableIndex >= 0 ? nFirstUsableIndex : -1)); m_pPressureGaugeCombo->setCurrentIndex(nCurrentIndex); m_pPressureGaugeCombo->setEnabled(nFirstUsableIndex >= 0); m_sPreviewPressureGaugeCode = nCurrentIndex >= 0 ? m_pPressureGaugeCombo->itemData(nCurrentIndex).toString() : QString(); updatePreviewComboToolTip(m_pPressureGaugeCombo); m_pPressureGaugeCombo->blockSignals(bSignalsWereBlocked); } void nmWellPressureFlowPage::populateFlowGaugeCombo() { if (m_pFlowGaugeCombo == nullptr) { return; } bool bSignalsWereBlocked = m_pFlowGaugeCombo->blockSignals(true); m_pFlowGaugeCombo->clear(); if (m_pWell == nullptr) { m_vecFlowGaugeRecords.clear(); m_pFlowGaugeCombo->setEnabled(false); m_pFlowGaugeCombo->blockSignals(bSignalsWereBlocked); return; } // 缓存整份记录列表供 currentFlowRecord() 安全返回内部指针,记录内容切换记录时不变。 m_vecFlowGaugeRecords = m_pWell->getFlowRecords(); int nPreviewIndex = -1; int nCalculationIndex = -1; int nFirstUsableIndex = -1; const QString sCalculationCode = m_pWell->getSelectedFlowGaugeCode(); for (int nIndex = 0; nIndex < m_vecFlowGaugeRecords.size(); ++nIndex) { const nmFlowGaugeRecord& oRecord = m_vecFlowGaugeRecords[nIndex]; appendGaugeComboItem(m_pFlowGaugeCombo, oRecord.sGaugeName, oRecord.sGaugeCode, oRecord.sGaugeTime, oRecord.eStatus); const int nItemIndex = m_pFlowGaugeCombo->count() - 1; if (oRecord.eStatus != NM_GaugeRecord_Usable) { continue; } if (nFirstUsableIndex < 0) { nFirstUsableIndex = nItemIndex; } if (oRecord.sGaugeCode == m_sPreviewFlowGaugeCode) { nPreviewIndex = nItemIndex; } if (oRecord.sGaugeCode == sCalculationCode) { nCalculationIndex = nItemIndex; } } const int nCurrentIndex = nPreviewIndex >= 0 ? nPreviewIndex : (nCalculationIndex >= 0 ? nCalculationIndex : (nFirstUsableIndex >= 0 ? nFirstUsableIndex : -1)); m_pFlowGaugeCombo->setCurrentIndex(nCurrentIndex); m_pFlowGaugeCombo->setEnabled(nFirstUsableIndex >= 0); m_sPreviewFlowGaugeCode = nCurrentIndex >= 0 ? m_pFlowGaugeCombo->itemData(nCurrentIndex).toString() : QString(); updatePreviewComboToolTip(m_pFlowGaugeCombo); m_pFlowGaugeCombo->blockSignals(bSignalsWereBlocked); } void nmWellPressureFlowPage::populateDisplayPhaseCombo( const nmFlowGaugeRecord* pRecord) { if (m_pDisplayPhaseCombo == nullptr || m_pDisplayPhaseContainer == nullptr) { return; } bool bSignalsWereBlocked = m_pDisplayPhaseCombo->blockSignals(true); m_pDisplayPhaseCombo->clear(); if (pRecord == nullptr) { m_pDisplayPhaseContainer->setVisible(false); m_eDisplayPhase = PHASE_UNKNOWN; m_pDisplayPhaseCombo->blockSignals(bSignalsWereBlocked); return; } // 仅列出记录中真实含数据的相;若记录只有 1 个相有数据,整行隐藏但仍取该相的点。 QVector vecAvailablePhases; QStringList listAvailableLabels; if (hasRealPhaseData(pRecord->vecOilPoints)) { vecAvailablePhases.append(PHASE_Oil); listAvailableLabels.append(tr("Oil")); } if (hasRealPhaseData(pRecord->vecGasPoints)) { vecAvailablePhases.append(PHASE_Gas); listAvailableLabels.append(tr("Gas")); } if (hasRealPhaseData(pRecord->vecWaterPoints)) { vecAvailablePhases.append(PHASE_Water); listAvailableLabels.append(tr("Water")); } if (vecAvailablePhases.size() <= 1) { m_pDisplayPhaseContainer->setVisible(false); m_eDisplayPhase = vecAvailablePhases.isEmpty() ? PHASE_UNKNOWN : vecAvailablePhases.first(); m_pDisplayPhaseCombo->blockSignals(bSignalsWereBlocked); return; } m_pDisplayPhaseContainer->setVisible(true); int nCurrentIndex = -1; for (int nIndex = 0; nIndex < vecAvailablePhases.size(); ++nIndex) { m_pDisplayPhaseCombo->addItem(listAvailableLabels[nIndex], static_cast(vecAvailablePhases[nIndex])); if (vecAvailablePhases[nIndex] == m_eDisplayPhase) { nCurrentIndex = nIndex; } } if (nCurrentIndex < 0) { NM_PHASE_TYPE eDefaultPhase = defaultDisplayPhase(pRecord); for (int nIndex = 0; nIndex < vecAvailablePhases.size(); ++nIndex) { if (vecAvailablePhases[nIndex] == eDefaultPhase) { nCurrentIndex = nIndex; break; } } } if (nCurrentIndex < 0) { nCurrentIndex = 0; } m_pDisplayPhaseCombo->setCurrentIndex(nCurrentIndex); m_eDisplayPhase = static_cast( m_pDisplayPhaseCombo->itemData(nCurrentIndex).toInt()); m_pDisplayPhaseCombo->blockSignals(bSignalsWereBlocked); } NM_PHASE_TYPE nmWellPressureFlowPage::defaultDisplayPhase( const nmFlowGaugeRecord* pRecord) const { if (pRecord == nullptr) { return PHASE_UNKNOWN; } // 优先使用井别对应的参考相,前提是该相在本条记录中确有真实数据。 NM_PHASE_TYPE eReferencePhase = (m_pWell != nullptr) ? m_pWell->getReferenceFlowPhase() : PHASE_UNKNOWN; if (eReferencePhase == PHASE_Oil && hasRealPhaseData(pRecord->vecOilPoints)) { return PHASE_Oil; } if (eReferencePhase == PHASE_Gas && hasRealPhaseData(pRecord->vecGasPoints)) { return PHASE_Gas; } if (eReferencePhase == PHASE_Water && hasRealPhaseData(pRecord->vecWaterPoints)) { return PHASE_Water; } // 参考相无数据时,退回记录中第一个有真实数据的相,固定按油、气、水顺序。 if (hasRealPhaseData(pRecord->vecOilPoints)) { return PHASE_Oil; } if (hasRealPhaseData(pRecord->vecGasPoints)) { return PHASE_Gas; } if (hasRealPhaseData(pRecord->vecWaterPoints)) { return PHASE_Water; } return PHASE_UNKNOWN; } const nmPressureGaugeRecord* nmWellPressureFlowPage::currentPressureRecord() const { for (int nIndex = 0; nIndex < m_vecPressureGaugeRecords.size(); ++nIndex) { if (m_vecPressureGaugeRecords[nIndex].sGaugeCode == m_sPreviewPressureGaugeCode) { return &m_vecPressureGaugeRecords[nIndex]; } } return nullptr; } const nmFlowGaugeRecord* nmWellPressureFlowPage::currentFlowRecord() const { for (int nIndex = 0; nIndex < m_vecFlowGaugeRecords.size(); ++nIndex) { if (m_vecFlowGaugeRecords[nIndex].sGaugeCode == m_sPreviewFlowGaugeCode) { return &m_vecFlowGaugeRecords[nIndex]; } } return nullptr; } void nmWellPressureFlowPage::initializeUnitSupport() { QStringList listTimeCandidates; listTimeCandidates << preferredTimeUnit() << QLatin1String("hr"); QStringList listFlowCandidates; listFlowCandidates << preferredFlowUnit() << QLatin1String("m^3/d") << QLatin1String("m^3/D"); // 优先使用当前单位包的h、m³/d;旧英文单位包自动兼容hr、m^3/d。 m_pTimeUnitGroup = resolveUnitGroup( listTimeCandidates, m_sTimeBaseUnit); m_pFlowUnitGroup = resolveUnitGroup( listFlowCandidates, m_sFlowBaseUnit); // 单位组缺失或当前单位不再存在时回退基准单位,入口由框架表头禁用。 if (m_pTimeUnitGroup == nullptr || m_pTimeUnitGroup->indexOf(m_sTimeUnit) < 0) { m_sTimeUnit = m_sTimeBaseUnit; } if (m_pFlowUnitGroup == nullptr || m_pFlowUnitGroup->indexOf(m_sFlowUnit) < 0) { m_sFlowUnit = m_sFlowBaseUnit; } updateUnitHeader(); } void nmWellPressureFlowPage::updateUnitHeader() { if (m_pFlowHeader == nullptr) { return; } QString sFlowTitle = tr("Flow"); switch (m_eDisplayPhase) { case PHASE_Oil: sFlowTitle = tr("Oil phase flow"); break; case PHASE_Gas: sFlowTitle = tr("Gas phase flow"); break; case PHASE_Water: sFlowTitle = tr("Water phase flow"); break; default: break; } QStringList listTitles; listTitles << tr("Time") << sFlowTitle; QStringList listUnits; listUnits << m_sTimeUnit << m_sFlowUnit; m_pFlowHeader->setHeaderData(listTitles, listUnits); } bool nmWellPressureFlowPage::buildFlowTableData( const QString& sTimeUnit, const QString& sFlowUnit, QVector& vecFlowPoints, int& nTimeDigit, int& nFlowDigit) const { vecFlowPoints = m_vecRawFlowPoints; // 空数据也必须验证目标单位,避免表头切换后留下不可用单位状态。 double dConvertedZero = 0.0; if (!convertValue(m_pTimeUnitGroup, m_sTimeBaseUnit, sTimeUnit, 0.0, dConvertedZero, &nTimeDigit) || !convertValue(m_pFlowUnitGroup, m_sFlowBaseUnit, sFlowUnit, 0.0, dConvertedZero, &nFlowDigit)) { return false; } // 表格每次从原始副本换算,禁止使用当前显示值继续换算。 for (int nIndex = 0; nIndex < vecFlowPoints.size(); ++nIndex) { double dDuration = 0.0; double dFlow = 0.0; if (!convertValue(m_pTimeUnitGroup, m_sTimeBaseUnit, sTimeUnit, m_vecRawFlowPoints[nIndex].x(), dDuration) || !convertValue(m_pFlowUnitGroup, m_sFlowBaseUnit, sFlowUnit, m_vecRawFlowPoints[nIndex].y(), dFlow)) { return false; } vecFlowPoints[nIndex] = QPointF(dDuration, dFlow); } return true; } bool nmWellPressureFlowPage::convertValue( iUnitGroup* pUnitGroup, const QString& sBaseUnit, const QString& sDestinationUnit, double dSourceValue, double& dDestinationValue, int* pDestinationDigit) const { int nDigit = 6; if (pUnitGroup != nullptr) { iUnitItem* pUnitItem = pUnitGroup->getUnitItem(sDestinationUnit); if (pUnitItem != nullptr && pUnitItem->m_nDigit >= 0) { nDigit = pUnitItem->m_nDigit; } } if (sDestinationUnit == sBaseUnit) { dDestinationValue = dSourceValue; if (pDestinationDigit != nullptr) { *pDestinationDigit = nDigit; } return true; } if (pUnitGroup == nullptr || pUnitGroup->indexOf(sBaseUnit) < 0 || pUnitGroup->indexOf(sDestinationUnit) < 0) { return false; } int nFallbackDigit = nDigit; bool bConverted = pUnitGroup->convert(sBaseUnit, dSourceValue, sDestinationUnit, dDestinationValue, nDigit); if (bConverted && pDestinationDigit != nullptr) { *pDestinationDigit = nDigit >= 0 ? nDigit : nFallbackDigit; } return bConverted; } void nmWellPressureFlowPage::updateViews() { QVector vecFlowChartPoints = createFlowChartPoints(); m_pPressureChart->setAxisUnits( m_sTimeBaseUnit, tr("MPa")); m_pFlowChart->setAxisUnits(m_sTimeBaseUnit, m_sFlowBaseUnit); m_pPressureChart->setData(m_vecRawPressurePoints); m_pFlowChart->setData(vecFlowChartPoints); updateXAxisRange(vecFlowChartPoints); updateFlowTable(); bool bPressureEmpty = m_vecRawPressurePoints.isEmpty(); bool bFlowEmpty = m_vecRawFlowPoints.isEmpty(); m_pPressureChartStack->setCurrentIndex(bPressureEmpty ? 1 : 0); m_pFlowChartStack->setCurrentIndex(bFlowEmpty ? 1 : 0); m_pFlowTableStack->setCurrentIndex(bFlowEmpty ? 1 : 0); m_pContentStack->setCurrentIndex( bPressureEmpty && bFlowEmpty ? 1 : 0); updateSelectedFlowSegment(); } QVector nmWellPressureFlowPage::createFlowChartPoints() const { QVector vecChartPoints; double dTotalTime = 0.0; for (int nIndex = 0; nIndex < m_vecRawFlowPoints.size(); ++nIndex) { const QPointF& oSegment = m_vecRawFlowPoints[nIndex]; if (nIndex == 0) { vecChartPoints.append(QPointF(0.0, oSegment.y())); } dTotalTime += oSegment.x(); vecChartPoints.append(QPointF(dTotalTime, oSegment.y())); if (nIndex < m_vecRawFlowPoints.size() - 1 && qAbs(m_vecRawFlowPoints[nIndex + 1].y() - oSegment.y()) > 1e-6) { vecChartPoints.append(QPointF( dTotalTime, m_vecRawFlowPoints[nIndex + 1].y())); } } return vecChartPoints; } void nmWellPressureFlowPage::updateFlowTable() { bool bSignalsWereBlocked = m_pFlowTable->blockSignals(true); m_pFlowTable->clearContents(); m_pFlowTable->setRowCount(m_vecTableFlowPoints.size()); // 纵向表头显示Qt默认的1~N序号,内部仍使用零基流量点索引。 for (int nIndex = 0; nIndex < m_vecTableFlowPoints.size(); ++nIndex) { QTableWidgetItem* pTimeItem = new QTableWidgetItem( displayValueText(m_vecTableFlowPoints[nIndex].x(), m_nTimeDigit)); pTimeItem->setTextAlignment(Qt::AlignCenter); m_pFlowTable->setItem(nIndex, 0, pTimeItem); QTableWidgetItem* pFlowItem = new QTableWidgetItem( displayValueText(m_vecTableFlowPoints[nIndex].y(), m_nFlowDigit)); pFlowItem->setTextAlignment(Qt::AlignCenter); m_pFlowTable->setItem(nIndex, 1, pFlowItem); } if (m_nSelectedFlowSegment >= 0 && m_nSelectedFlowSegment < m_pFlowTable->rowCount()) { m_pFlowTable->setCurrentCell(m_nSelectedFlowSegment, 0); m_pFlowTable->selectRow(m_nSelectedFlowSegment); } else { m_nSelectedFlowSegment = -1; m_pFlowTable->setCurrentCell(-1, -1); m_pFlowTable->clearSelection(); } m_pFlowTable->blockSignals(bSignalsWereBlocked); } void nmWellPressureFlowPage::updateXAxisRange( const QVector& vecFlowChartPoints) { bool bHasRange = false; double dMinimum = 0.0; double dMaximum = 0.0; for (int nIndex = 0; nIndex < m_vecRawPressurePoints.size(); ++nIndex) { double dTime = m_vecRawPressurePoints[nIndex].x(); dMinimum = bHasRange ? qMin(dMinimum, dTime) : dTime; dMaximum = bHasRange ? qMax(dMaximum, dTime) : dTime; bHasRange = true; } for (int nIndex = 0; nIndex < vecFlowChartPoints.size(); ++nIndex) { double dTime = vecFlowChartPoints[nIndex].x(); dMinimum = bHasRange ? qMin(dMinimum, dTime) : dTime; dMaximum = bHasRange ? qMax(dMaximum, dTime) : dTime; bHasRange = true; } if (bHasRange) { m_pPressureChart->setXAxisRange(dMinimum, dMaximum); m_pFlowChart->setXAxisRange(dMinimum, dMaximum); } else { m_pPressureChart->clearXAxisRange(); m_pFlowChart->clearXAxisRange(); } } void nmWellPressureFlowPage::updateSelectedFlowSegment() { if (m_nSelectedFlowSegment < 0 || m_nSelectedFlowSegment >= m_vecRawFlowPoints.size()) { m_pPressureChart->clearHighlightedTimeRange(); m_pFlowChart->clearHighlightedTimeRange(); return; } // 流量点X值是持续时间,段区间必须从第0段开始逐段累加。 double dStartTime = 0.0; for (int nIndex = 0; nIndex < m_nSelectedFlowSegment; ++nIndex) { dStartTime += m_vecRawFlowPoints[nIndex].x(); } double dEndTime = dStartTime + m_vecRawFlowPoints[m_nSelectedFlowSegment].x(); m_pPressureChart->setHighlightedTimeRange( dStartTime, dEndTime); m_pFlowChart->setHighlightedTimeRange(dStartTime, dEndTime); } void nmWellPressureFlowPage::applyFlowSegmentSelection(int nSegmentIndex) { if (nSegmentIndex < 0 || nSegmentIndex >= m_vecRawFlowPoints.size() || nSegmentIndex >= m_pFlowTable->rowCount()) { nSegmentIndex = -1; } m_nSelectedFlowSegment = nSegmentIndex; bool bSignalsWereBlocked = m_pFlowTable->blockSignals(true); if (m_nSelectedFlowSegment >= 0) { m_pFlowTable->setCurrentCell(m_nSelectedFlowSegment, 0); m_pFlowTable->selectRow(m_nSelectedFlowSegment); QTableWidgetItem* pItem = m_pFlowTable->item( m_nSelectedFlowSegment, 0); if (pItem != nullptr) { m_pFlowTable->scrollToItem( pItem, QAbstractItemView::EnsureVisible); } } else { m_pFlowTable->setCurrentCell(-1, -1); m_pFlowTable->clearSelection(); } m_pFlowTable->blockSignals(bSignalsWereBlocked); updateSelectedFlowSegment(); } int nmWellPressureFlowPage::findFlowSegmentByTableTime( double dTimePoint) const { double dTotalTime = 0.0; for (int nIndex = 0; nIndex < m_vecTableFlowPoints.size(); ++nIndex) { dTotalTime += qMax(0.0, m_vecTableFlowPoints[nIndex].x()); } const double dTolerance = qMax(1.0, qAbs(dTotalTime)) * 1e-9; if (dTimePoint < 0.0 || dTimePoint > dTotalTime + dTolerance) { return -1; } // 零时长点不占区间;分界点继续查找后一有效段。 double dStartTime = 0.0; int nLastValidSegment = -1; for (int nIndex = 0; nIndex < m_vecTableFlowPoints.size(); ++nIndex) { double dDuration = m_vecTableFlowPoints[nIndex].x(); if (dDuration <= dTolerance) { continue; } nLastValidSegment = nIndex; double dEndTime = dStartTime + dDuration; if (dTimePoint < dEndTime - dTolerance) { return nIndex; } dStartTime = dEndTime; } // 最终结束点没有后一段,归入最后一个有效流量段。 if (nLastValidSegment >= 0 && qAbs(dTimePoint - dTotalTime) <= dTolerance) { return nLastValidSegment; } return -1; } void nmWellPressureFlowPage::resetDataViews() { m_vecRawPressurePoints.clear(); m_vecRawFlowPoints.clear(); m_vecTableFlowPoints.clear(); m_nSelectedFlowSegment = -1; if (m_pWell == nullptr) { m_sPreviewPressureGaugeCode.clear(); m_sPreviewFlowGaugeCode.clear(); } // 井绑定切换或清空时先清空并禁用三个记录/相下拉框,等待 refreshData() 重新填充。 if (m_pPressureGaugeCombo != nullptr) { bool bSignalsWereBlocked = m_pPressureGaugeCombo->blockSignals(true); m_pPressureGaugeCombo->clear(); m_pPressureGaugeCombo->setEnabled(false); m_pPressureGaugeCombo->setToolTip(QString()); m_pPressureGaugeCombo->blockSignals(bSignalsWereBlocked); } if (m_pFlowGaugeCombo != nullptr) { bool bSignalsWereBlocked = m_pFlowGaugeCombo->blockSignals(true); m_pFlowGaugeCombo->clear(); m_pFlowGaugeCombo->setEnabled(false); m_pFlowGaugeCombo->setToolTip(QString()); m_pFlowGaugeCombo->blockSignals(bSignalsWereBlocked); } m_vecPressureGaugeRecords.clear(); m_vecFlowGaugeRecords.clear(); if (m_pDisplayPhaseCombo != nullptr) { bool bSignalsWereBlocked = m_pDisplayPhaseCombo->blockSignals(true); m_pDisplayPhaseCombo->clear(); m_pDisplayPhaseCombo->blockSignals(bSignalsWereBlocked); } if (m_pDisplayPhaseContainer != nullptr) { m_pDisplayPhaseContainer->setVisible(false); } m_eDisplayPhase = PHASE_UNKNOWN; updateViews(); } QString nmWellPressureFlowPage::buildDiagnosticText() const { if (m_pWell == nullptr) { return tr("No well is bound to this page."); } QStringList listLines; // 井基础信息:全部即时从井对象重新读取,验证框架解析结果而非页面缓存。 listLines << tr("=== Well ==="); listLines << tr("Well name: %1") .arg(diagTextOrNone(m_pWell->getWellName())); listLines << tr("Well code: %1") .arg(diagTextOrNone(m_pWell->getWellCode())); listLines << tr("Well category: %1") .arg(diagWellCategoryText(m_pWell->getWellCategory())); listLines << tr("Reference flow phase: %1") .arg(diagPhaseText(m_pWell->getReferenceFlowPhase())); listLines << QString(); // 压力记录摘要:枚举全部记录,含损坏、空与身份无效记录。 QVector vecPressureRecords = m_pWell->getPressureRecords(); listLines << tr("=== Pressure records ==="); listLines << tr("Total pressure records: %1") .arg(vecPressureRecords.size()); listLines << tr("Selected pressure gauge code: %1") .arg(diagTextOrNone(m_pWell->getSelectedPressureGaugeCode())); for (int nIndex = 0; nIndex < vecPressureRecords.size(); ++nIndex) { const nmPressureGaugeRecord& oRecord = vecPressureRecords[nIndex]; listLines << QString(); listLines << tr("[%1] Name: %2") .arg(nIndex + 1) .arg(diagTextOrNone(oRecord.sGaugeName)); listLines << QLatin1String(" ") + tr("Code: %1").arg(diagTextOrNone(oRecord.sGaugeCode)); listLines << QLatin1String(" ") + tr("Time: %1").arg(diagTextOrNone(oRecord.sGaugeTime)); listLines << QLatin1String(" ") + tr("Status: %1").arg(diagStatusText(oRecord.eStatus)); listLines << QLatin1String(" ") + tr("Point count: %1").arg(oRecord.vecPressurePoints.size()); if (oRecord.vecPressurePoints.isEmpty()) { listLines << QLatin1String(" ") + tr("No pressure points."); } else { listLines << QLatin1String(" ") + tr("First point = %1, last point = %2") .arg(diagPointText(oRecord.vecPressurePoints.first())) .arg(diagPointText(oRecord.vecPressurePoints.last())); } } listLines << QString(); // 流量记录摘要:油、气、水三相分别统计,多相记录额外检查相间时间一致性。 QVector vecFlowRecords = m_pWell->getFlowRecords(); listLines << tr("=== Flow records ==="); listLines << tr("Total flow records: %1").arg(vecFlowRecords.size()); listLines << tr("Selected flow gauge code: %1") .arg(diagTextOrNone(m_pWell->getSelectedFlowGaugeCode())); listLines << tr("Current display phase: %1") .arg(diagPhaseText(m_eDisplayPhase)); for (int nIndex = 0; nIndex < vecFlowRecords.size(); ++nIndex) { const nmFlowGaugeRecord& oRecord = vecFlowRecords[nIndex]; listLines << QString(); listLines << tr("[%1] Name: %2") .arg(nIndex + 1) .arg(diagTextOrNone(oRecord.sGaugeName)); listLines << QLatin1String(" ") + tr("Code: %1").arg(diagTextOrNone(oRecord.sGaugeCode)); listLines << QLatin1String(" ") + tr("Time: %1").arg(diagTextOrNone(oRecord.sGaugeTime)); listLines << QLatin1String(" ") + tr("Status: %1").arg(diagStatusText(oRecord.eStatus)); listLines << QLatin1String(" ") + tr("Multiphase: %1") .arg(oRecord.bMultiPhase ? tr("Yes") : tr("No")); listLines << QLatin1String(" ") + tr("Own flow segment index (1-based): %1") .arg(oRecord.nIndexF); appendPhaseDiagLines(listLines, tr("Oil"), oRecord.vecOilPoints); appendPhaseDiagLines(listLines, tr("Gas"), oRecord.vecGasPoints); appendPhaseDiagLines(listLines, tr("Water"), oRecord.vecWaterPoints); if (oRecord.bMultiPhase) { listLines << QLatin1String(" ") + tr("Phase time consistency: %1") .arg(diagPhaseConsistencyText(oRecord)); } } // 数值记录显示 Corrupt 时继续向下追溯对应框架井的原始存储,尤其检查 // 另一口井自己的 GaugeDataEx2 是否确实为 N 行 x 4 列。 appendFrameworkFlowStorageDiagnostic( listLines, m_pWell->getWellCode(), m_pWell->getSelectedFlowGaugeCode()); // 单独读取当前流动段分析快照,验证“多相数据”弹框保存的矩阵是否位于 // ZxSegmentInfo::m_vvecMpData。此处只展示,不覆盖工程井记录或求解输入。 listLines << QString(); listLines << tr("=== Current flow-segment multiphase data ==="); nmDataAnalyzeContextProvider* pContextProvider = nmDataAnalyzeContext::provider(); iSubWndFitting* pSubWndFitting = nmDataAnalyzeManager::getCurrentFitting(); nmDataAnalyzeManager* pCurrentManager = nmDataAnalyzeManager::getCurrentInstance(); const QString sCurrentPrimaryWellCode = pCurrentManager != nullptr ? pCurrentManager->getPrimaryWellCode() : QString(); const bool bSnapshotBelongsToEditedWell = !sCurrentPrimaryWellCode.isEmpty() && sCurrentPrimaryWellCode == m_pWell->getWellCode(); listLines << tr("Current fitting primary well code: %1") .arg(diagTextOrNone(sCurrentPrimaryWellCode)); listLines << tr("Snapshot belongs to edited well: %1") .arg(bSnapshotBelongsToEditedWell ? tr("Yes") : tr("No")); if (!bSnapshotBelongsToEditedWell) { listLines << tr("This snapshot belongs to the current fitting primary well and does not override the edited other well."); } bool bSegmentMultiPhase = false; QString sFlowCurveName; VVecDouble vvecMultiPhaseData; const bool bSnapshotAvailable = pContextProvider != nullptr && pSubWndFitting != nullptr && pContextProvider->getCurrentSegmentMultiPhaseData( pSubWndFitting, bSegmentMultiPhase, sFlowCurveName, vvecMultiPhaseData); listLines << tr("Snapshot available: %1") .arg(bSnapshotAvailable ? tr("Yes") : tr("No")); if (bSnapshotAvailable) { PvtFluidType eFluidType = WFT_Null; const bool bFluidTypeAvailable = pContextProvider->getBasicPft(pSubWndFitting, eFluidType); listLines << tr("PVT phase type: %1") .arg(bFluidTypeAvailable ? diagFluidTypeText(eFluidType) : tr("Unknown")); listLines << tr("Segment multiphase flag: %1") .arg(bSegmentMultiPhase ? tr("Yes") : tr("No")); listLines << tr("Flow curve name: %1") .arg(diagTextOrNone(sFlowCurveName)); listLines << tr("Matrix vector count: %1") .arg(vvecMultiPhaseData.size()); QStringList listVectorSizes; for (int nVectorIndex = 0; nVectorIndex < vvecMultiPhaseData.size(); ++nVectorIndex) { listVectorSizes << QString("[%1] = %2") .arg(nVectorIndex) .arg(vvecMultiPhaseData[nVectorIndex].size()); } listLines << tr("Vector sizes: %1") .arg(listVectorSizes.isEmpty() ? tr("None") : listVectorSizes.join(QLatin1String(", "))); bool bMatrixShapeValid = vvecMultiPhaseData.size() == 4; int nPointCount = bMatrixShapeValid ? vvecMultiPhaseData[0].size() : 0; for (int nVectorIndex = 1; bMatrixShapeValid && nVectorIndex < 4; ++nVectorIndex) { if (vvecMultiPhaseData[nVectorIndex].size() != nPointCount) { bMatrixShapeValid = false; } } listLines << tr("Four-vector shape valid: %1") .arg(bMatrixShapeValid ? tr("Yes") : tr("No")); if (bMatrixShapeValid) { listLines << tr("Data rows (time, oil, gas, water):"); const int nMaxListedRows = 64; const int nListedRows = qMin(nPointCount, nMaxListedRows); for (int nPointIndex = 0; nPointIndex < nListedRows; ++nPointIndex) { listLines << QString(" [%1] t=%2, oil=%3, gas=%4, water=%5") .arg(nPointIndex + 1) .arg(diagNumberText( vvecMultiPhaseData[0][nPointIndex])) .arg(diagNumberText( vvecMultiPhaseData[1][nPointIndex])) .arg(diagNumberText( vvecMultiPhaseData[2][nPointIndex])) .arg(diagNumberText( vvecMultiPhaseData[3][nPointIndex])); } if (nPointCount > nListedRows) { listLines << tr(" ... %1 additional rows omitted") .arg(nPointCount - nListedRows); } } } return listLines.join(QLatin1String("\n")); }