diff --git a/Include/nmNum/nmData/nmDataAnalyzeManager.h b/Include/nmNum/nmData/nmDataAnalyzeManager.h index 3705fb43..fe3f8333 100644 --- a/Include/nmNum/nmData/nmDataAnalyzeManager.h +++ b/Include/nmNum/nmData/nmDataAnalyzeManager.h @@ -828,6 +828,12 @@ public: private: /** @brief 按 UUID 给当前实时井统一绑定或解除最后结果快照弱引用。 */ void rebindPebiResultSnapshotToWells(); + /** @brief 在候选 Manager 内完整读取并校验一套 v3 成果。 */ + bool loadNmResultV3InPlace(const QString& sLoadAnalDir); + /** @brief 通过交换所有权一次发布已完整校验的候选项目状态。 */ + void swapLoadedProjectState(nmDataAnalyzeManager& oLoadedManager); + /** @brief 项目状态替换后用新对象地址重建属性注册表。 */ + void rebuildLoadedAttributeRegistry(); /** @brief 创建本数据管理器的成果窗口;只用于读取本成果上下文,不拥有对象。 */ iSubWndFitting* m_pOwnerFitting; /** @brief 保护后台使用计数和析构等待条件。 */ diff --git a/Include/nmNum/nmData/nmDataOutline.h b/Include/nmNum/nmData/nmDataOutline.h index bf903162..0483af16 100644 --- a/Include/nmNum/nmData/nmDataOutline.h +++ b/Include/nmNum/nmData/nmDataOutline.h @@ -65,8 +65,9 @@ class NM_DATA_EXPORT nmDataOutline : public nmDataBase { int getBackgrdAlpha() const; void setBackgrdAlpha(int nAlpha); - // 视觉属性是否有效(仅从已保存文件加载后为true,新建时为false) + // 视觉属性是否有效;未创建过Map图元的边界保持false。 bool hasVisualProps() const; + // 设置视觉属性有效状态,图元完成样式同步后置为true。 void setVisualPropsValid(bool bValid); signals: @@ -115,7 +116,7 @@ class NM_DATA_EXPORT nmDataOutline : public nmDataBase { double m_dPenWidth; // 画笔宽度 QColor m_clrBackgrd; // 填充背景色 int m_nBackgrdAlpha; // 填充透明度 (0-255) - bool m_bHasVisualProps; // 视觉属性是否有效(从文件加载后为true) + bool m_bHasVisualProps; // 视觉属性是否已由Map图元初始化 }; #endif // NMDATARECTOUTLINE_H diff --git a/Include/nmNum/nmData/nmNumericalSaveSession.h b/Include/nmNum/nmData/nmNumericalSaveSession.h index df7e47c6..4e13719e 100644 --- a/Include/nmNum/nmData/nmNumericalSaveSession.h +++ b/Include/nmNum/nmData/nmNumericalSaveSession.h @@ -67,6 +67,15 @@ public: QString& sMainJsonPath, nmNumericalFileReference* pMainReference = NULL, QString* pError = NULL); + /** + * @brief 校验已发布根清单,并查询清单是否明确引用指定窗口。 + * @param bContainsWindow 返回窗口是否存在;不存在不是成果损坏。 + */ + static bool containsPublishedWindow( + const QString& sResultRootDirectory, + const QString& sWindowId, + bool& bContainsWindow, + QString* pError = NULL); /** @brief 只校验根清单结构、窗口唯一性和全部主 JSON 哈希。 */ static bool validatePublishedManifest( const QString& sResultRootDirectory, diff --git a/Include/nmNum/nmSubWnd/nmSubWndUtils.h b/Include/nmNum/nmSubWnd/nmSubWndUtils.h index 81b5358b..98281c49 100644 --- a/Include/nmNum/nmSubWnd/nmSubWndUtils.h +++ b/Include/nmNum/nmSubWnd/nmSubWndUtils.h @@ -154,6 +154,8 @@ public: void slotHandleMdiClosing(bool& bAllowClose); // 外层成果窗口析构后,移除保存的关闭状态 void slotHandleMdiDestroyed(QObject* pObject); + // 当前外层保存调用栈结束后,允许下一次保存重新发布完整代次 + void slotClearCompletedNumericalSaveRoots(); protected: @@ -191,6 +193,8 @@ public: /** @brief 当前显式跨窗口保存会话;析构时自动中止未提交代次。 */ nmNumericalSaveSession* m_pNumericalSaveSession; + /** @brief 本轮外层回调中已经完整发布的成果根目录,避免逐窗口重复写入。 */ + QSet m_setCompletedNumericalSaveRoots; }; /** diff --git a/Src/nmNum/nmData/nmDataAnalyzeManager.cpp b/Src/nmNum/nmData/nmDataAnalyzeManager.cpp index cb823754..f1341b97 100644 --- a/Src/nmNum/nmData/nmDataAnalyzeManager.cpp +++ b/Src/nmNum/nmData/nmDataAnalyzeManager.cpp @@ -77,6 +77,7 @@ #include #include #include +#include // 井基参数名 → 数据成员访问器的映射描述 struct WellParaDesc { @@ -4742,12 +4743,15 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath) // 第二步:先从 JSON 井数组建立唯一 WellCode 到井型的索引,尚不修改当前内存数据。 QMap mapSavedWellTypes; + QSet setSavedWellInstanceIds; const rapidjson::Value& vecWellsJson = doc["Wells"]; for(rapidjson::SizeType nIndex = 0; nIndex < vecWellsJson.Size(); ++nIndex) { const rapidjson::Value& oWellJson = vecWellsJson[nIndex]; if(!oWellJson.IsObject() || !oWellJson.HasMember("WellCode") || !oWellJson["WellCode"].IsString() || + !oWellJson.HasMember("WellInstanceId") || + !oWellJson["WellInstanceId"].IsString() || !oWellJson.HasMember("WellType") || !oWellJson["WellType"].IsInt()) { qWarning() << "Numerical project contains an invalid well entry:" << filePath; @@ -4756,6 +4760,11 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath) const QString sWellCode = QString::fromUtf8( oWellJson["WellCode"].GetString()); + const QString sWellInstanceId = QString::fromUtf8( + oWellJson["WellInstanceId"].GetString()); + const QUuid oWellInstanceUuid(sWellInstanceId); + const QString sNormalizedWellInstanceId = + oWellInstanceUuid.toString().remove('{').remove('}'); const NM_WELL_MODEL eWellType = static_cast( oWellJson["WellType"].GetInt()); const bool bSupportedWellType = @@ -4763,12 +4772,16 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath) eWellType == NM_WELL_MODEL::Vertical_Fractured_Well || eWellType == NM_WELL_MODEL::Horizontal_Fractured_Well; if(sWellCode.isEmpty() || !bSupportedWellType || - mapSavedWellTypes.contains(sWellCode)) { - qWarning() << "Numerical project contains an empty, duplicate or unsupported WellCode:" - << sWellCode; + mapSavedWellTypes.contains(sWellCode) || + oWellInstanceUuid.isNull() || + sNormalizedWellInstanceId != sWellInstanceId || + setSavedWellInstanceIds.contains(sWellInstanceId)) { + qWarning() << "Numerical project contains an invalid well identity:" + << sWellCode << sWellInstanceId; return false; } mapSavedWellTypes.insert(sWellCode, eWellType); + setSavedWellInstanceIds.insert(sWellInstanceId); } if(!mapSavedWellTypes.contains(sSavedPrimaryWellCode)) { qWarning() << "Primary WellCode is absent from Wells:" << sSavedPrimaryWellCode; @@ -5391,17 +5404,24 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath) // 反序列化函数没有返回值,因此必须再次核对实际创建的井目录。 // JSON 预校验目录与内存目录必须在 WellCode、井型和数量上完全一致。 QMap mapLoadedWellTypes; + QSet setLoadedWellInstanceIds; for(int nIndex = 0; nIndex < m_vWellData.size(); ++nIndex) { nmDataWellBase* pWellData = m_vWellData[nIndex]; if(pWellData == nullptr || pWellData->getWellCode().isEmpty() || - mapLoadedWellTypes.contains(pWellData->getWellCode())) { + mapLoadedWellTypes.contains(pWellData->getWellCode()) || + !setSavedWellInstanceIds.contains( + pWellData->getWellInstanceId()) || + setLoadedWellInstanceIds.contains( + pWellData->getWellInstanceId())) { qWarning() << "Loaded numerical project contains an invalid WellCode:"; return false; } mapLoadedWellTypes.insert(pWellData->getWellCode(), pWellData->getWellType()); + setLoadedWellInstanceIds.insert(pWellData->getWellInstanceId()); } - if(mapLoadedWellTypes != mapSavedWellTypes) { + if(mapLoadedWellTypes != mapSavedWellTypes || + setLoadedWellInstanceIds != setSavedWellInstanceIds) { qWarning() << "Loaded well directory differs from validated JSON:" << filePath; return false; @@ -5475,15 +5495,25 @@ bool nmDataAnalyzeManager::WriteProjectData( } // 第一步:保存前校验 Map 井目录,持久化身份只允许非空且唯一的 WellCode。 QSet setWellCodes; + QSet setWellInstanceIds; QMap mapWellTypes; for(int nIndex = 0; nIndex < m_vWellData.size(); ++nIndex) { nmDataWellBase* pWellData = m_vWellData[nIndex]; + const QString sWellInstanceId = pWellData == nullptr + ? QString() : pWellData->getWellInstanceId(); + const QUuid oWellInstanceUuid(sWellInstanceId); + const QString sNormalizedWellInstanceId = + oWellInstanceUuid.toString().remove('{').remove('}'); if(pWellData == nullptr || pWellData->getWellCode().isEmpty() || - setWellCodes.contains(pWellData->getWellCode())) { - qWarning() << "Cannot save numerical project: empty or duplicate WellCode."; + setWellCodes.contains(pWellData->getWellCode()) || + oWellInstanceUuid.isNull() || + sNormalizedWellInstanceId != sWellInstanceId || + setWellInstanceIds.contains(sWellInstanceId)) { + qWarning() << "Cannot save numerical project: invalid well identity."; return false; } setWellCodes.insert(pWellData->getWellCode()); + setWellInstanceIds.insert(sWellInstanceId); mapWellTypes.insert(pWellData->getWellCode(), pWellData->getWellType()); } @@ -6319,6 +6349,39 @@ bool nmDataAnalyzeManager::saveNmResult(QString sRstCode, iSubWndFitting* pSubWn } bool nmDataAnalyzeManager::loadNmResult(QString sLoadAnalDir) +{ + try { + // v3 先在独立候选对象上完成全部解析和业务校验。 + // 任一步失败时只析构候选,当前窗口已经发布的数据保持不变。 + nmDataAnalyzeManager oLoadedManager; + oLoadedManager.m_pOwnerFitting = m_pOwnerFitting; + oLoadedManager.initPvtParaFromSubFit(); + if(!oLoadedManager.loadNmResultV3InPlace(sLoadAnalDir)) { + return false; + } + + // 井构造时不能依赖尚未发布的候选 Manager,完整读取后统一绑定储层。 + for(int nIndex = 0; nIndex < oLoadedManager.m_vWellData.size(); ++nIndex) { + nmDataWellBase* pWell = oLoadedManager.m_vWellData[nIndex]; + if(pWell != NULL) { + pWell->m_pReservoir = oLoadedManager.m_reservoirData; + } + } + + swapLoadedProjectState(oLoadedManager); + rebuildLoadedAttributeRegistry(); + rebindPebiResultSnapshotToWells(); + emit sigPebiResultSnapshotChanged(); + return true; + } + catch(const std::bad_alloc&) { + qWarning() << "Not enough memory to stage numerical v3 result."; + return false; + } +} + +bool nmDataAnalyzeManager::loadNmResultV3InPlace( + const QString& sLoadAnalDir) { try { // 外部框架仍传入旧的“成果根/窗口标识”逻辑路径;v3 只从根清单解析真实代次。 @@ -6714,6 +6777,78 @@ bool nmDataAnalyzeManager::loadNmResult(QString sLoadAnalDir) #endif } +void nmDataAnalyzeManager::swapLoadedProjectState( + nmDataAnalyzeManager& oLoadedManager) +{ + // 这里只交换项目数据所有权。窗口、Plot、后台任务和同步原语仍属于当前 Manager。 + qSwap(m_vWellData, oLoadedManager.m_vWellData); + qSwap(m_vFaultData, oLoadedManager.m_vFaultData); + qSwap(m_vFractureData, oLoadedManager.m_vFractureData); + qSwap(m_vRegionData, oLoadedManager.m_vRegionData); + qSwap(m_vRegionMarkData, oLoadedManager.m_vRegionMarkData); + qSwap(m_vecLayers, oLoadedManager.m_vecLayers); + qSwap(m_vecPropertyInterpolationDataSets, + oLoadedManager.m_vecPropertyInterpolationDataSets); + + qSwap(m_outlineData, oLoadedManager.m_outlineData); + qSwap(m_axisData, oLoadedManager.m_axisData); + qSwap(m_pAutomaticFittingData, + oLoadedManager.m_pAutomaticFittingData); + qSwap(m_reservoirData, oLoadedManager.m_reservoirData); + qSwap(m_pGeoRefData, oLoadedManager.m_pGeoRefData); + qSwap(m_pSensitiveData, oLoadedManager.m_pSensitiveData); + qSwap(m_pebiPvtPara, oLoadedManager.m_pebiPvtPara); + qSwap(m_pMixedResults, oLoadedManager.m_pMixedResults); + qSwap(m_pTimeStep, oLoadedManager.m_pTimeStep); + qSwap(m_pCurDataWell, oLoadedManager.m_pCurDataWell); + + qSwap(m_eSolverModelType, oLoadedManager.m_eSolverModelType); + qSwap(m_nPebiSolverType, oLoadedManager.m_nPebiSolverType); + qSwap(m_nPebiOmpThreads, oLoadedManager.m_nPebiOmpThreads); + qSwap(m_nPebiIluReuseSteps, oLoadedManager.m_nPebiIluReuseSteps); + qSwap(m_oNumericalAnalysisCase, + oLoadedManager.m_oNumericalAnalysisCase); + qSwap(m_pPebiResultSnapshot, + oLoadedManager.m_pPebiResultSnapshot); + qSwap(m_sCurrentResultWellInstanceId, + oLoadedManager.m_sCurrentResultWellInstanceId); + + vtkSmartPointer pGrid = + m_pVtkUnstructuredGrid; + m_pVtkUnstructuredGrid = oLoadedManager.m_pVtkUnstructuredGrid; + oLoadedManager.m_pVtkUnstructuredGrid = pGrid; + pGrid = m_pResultBaseGrid; + m_pResultBaseGrid = oLoadedManager.m_pResultBaseGrid; + oLoadedManager.m_pResultBaseGrid = pGrid; + qSwap(m_mapTimeStepDataP, oLoadedManager.m_mapTimeStepDataP); + qSwap(m_mapWellLocations, oLoadedManager.m_mapWellLocations); + qSwap(m_dScalarRangeP[0], oLoadedManager.m_dScalarRangeP[0]); + qSwap(m_dScalarRangeP[1], oLoadedManager.m_dScalarRangeP[1]); + qSwap(m_bIsLoadData, oLoadedManager.m_bIsLoadData); +} + +void nmDataAnalyzeManager::rebuildLoadedAttributeRegistry() +{ + if(m_pAttrRegistry == NULL) { + return; + } + + // 注册表对象本身不替换,避免破坏已经建立的 UI 信号连接。 + m_pAttrRegistry->clear(); + if(m_reservoirData != NULL) { + m_pAttrRegistry->regAttr("h", &m_reservoirData->getThickness()); + m_pAttrRegistry->regAttr("Pi", &m_reservoirData->getInitialPressure()); + m_pAttrRegistry->regAttr("K", &m_reservoirData->getPermeability()); + m_pAttrRegistry->regAttr("phi", &m_reservoirData->getPorosity()); + m_pAttrRegistry->regAttr("Cti", &m_reservoirData->getCt()); + m_pAttrRegistry->regAttr("Cf", &m_reservoirData->getCf()); + m_pAttrRegistry->regAttr("Soi", &m_reservoirData->getSoi()); + m_pAttrRegistry->regAttr("Swi", &m_reservoirData->getSwi()); + } + syncWellAttrs(); + syncGeometryAttrs(); +} + bool nmDataAnalyzeManager::ensureDirectoryExists(const QString & dirPath) { QDir dir(dirPath); diff --git a/Src/nmNum/nmData/nmDataOutline.cpp b/Src/nmNum/nmData/nmDataOutline.cpp index 4e70e9fc..b7e686af 100644 --- a/Src/nmNum/nmData/nmDataOutline.cpp +++ b/Src/nmNum/nmData/nmDataOutline.cpp @@ -98,6 +98,9 @@ rapidjson::Value nmDataOutline::ToJsonValue(rapidjson::Document::AllocatorType& outlineObject.AddMember("PlotVisible", m_bPlotVisible, allocator); // 使用m_bPlotVisible // 序列化视觉属性 + // 未打开Map时,边界只有几何数据,视觉属性尚未由图元初始化。 + // 必须显式保存该状态,避免加载时把默认的0线宽误当成有效样式。 + outlineObject.AddMember("HasVisualProps", m_bHasVisualProps, allocator); outlineObject.AddMember("PenWidth", m_dPenWidth, allocator); outlineObject.AddMember("BackgrdR", m_clrBackgrd.red(), allocator); outlineObject.AddMember("BackgrdG", m_clrBackgrd.green(), allocator); @@ -163,22 +166,43 @@ void nmDataOutline::FromJsonValue(const rapidjson::Value& jsonValue) m_bPlotVisible = jsonValue["PlotVisible"].GetBool(); // 使用m_bPlotVisible } - // 反序列化视觉属性 - bool bHasVisual = false; + // 反序列化视觉属性。早期v3没有HasVisualProps,线宽大于0时才认为 + // 样式有效;这样可直接修复“未打开Map就保存”产生的0线宽成果。 + bool bVisualFieldsComplete = true; if (jsonValue.HasMember("PenWidth") && jsonValue["PenWidth"].IsNumber()) { m_dPenWidth = jsonValue["PenWidth"].GetDouble(); - bHasVisual = true; + } else { + bVisualFieldsComplete = false; } int r = 0, g = 0, b = 255; - if (jsonValue.HasMember("BackgrdR") && jsonValue["BackgrdR"].IsInt()) r = jsonValue["BackgrdR"].GetInt(); - if (jsonValue.HasMember("BackgrdG") && jsonValue["BackgrdG"].IsInt()) g = jsonValue["BackgrdG"].GetInt(); - if (jsonValue.HasMember("BackgrdB") && jsonValue["BackgrdB"].IsInt()) b = jsonValue["BackgrdB"].GetInt(); + if (jsonValue.HasMember("BackgrdR") && jsonValue["BackgrdR"].IsInt()) { + r = jsonValue["BackgrdR"].GetInt(); + } else { + bVisualFieldsComplete = false; + } + if (jsonValue.HasMember("BackgrdG") && jsonValue["BackgrdG"].IsInt()) { + g = jsonValue["BackgrdG"].GetInt(); + } else { + bVisualFieldsComplete = false; + } + if (jsonValue.HasMember("BackgrdB") && jsonValue["BackgrdB"].IsInt()) { + b = jsonValue["BackgrdB"].GetInt(); + } else { + bVisualFieldsComplete = false; + } m_clrBackgrd = QColor(r, g, b); if (jsonValue.HasMember("BackgrdAlpha") && jsonValue["BackgrdAlpha"].IsInt()) { m_nBackgrdAlpha = jsonValue["BackgrdAlpha"].GetInt(); - bHasVisual = true; + } else { + bVisualFieldsComplete = false; } - m_bHasVisualProps = bHasVisual; + const bool bSavedVisualProps = + jsonValue.HasMember("HasVisualProps") && + jsonValue["HasVisualProps"].IsBool() + ? jsonValue["HasVisualProps"].GetBool() + : true; + m_bHasVisualProps = bSavedVisualProps && + bVisualFieldsComplete && m_dPenWidth > 0.0; syncGeometryAttributes(); } diff --git a/Src/nmNum/nmData/nmDataWellBase.cpp b/Src/nmNum/nmData/nmDataWellBase.cpp index cfb1dd50..b69e5976 100644 --- a/Src/nmNum/nmData/nmDataWellBase.cpp +++ b/Src/nmNum/nmData/nmDataWellBase.cpp @@ -4,6 +4,7 @@ #include "nmPebiResultSnapshot.h" #include +#include #include #include #include @@ -19,14 +20,20 @@ nmDataWellBase::nmDataWellBase() , m_pReservoir(nullptr) { m_sWellInstanceId = createWellInstanceId(); - m_pReservoir = nmDataAnalyzeManager::getCurrentInstance()->getReservoirData(); + nmDataAnalyzeManager* pDataManager = + nmDataAnalyzeManager::getCurrentInstance(); + m_pReservoir = pDataManager == nullptr + ? nullptr : pDataManager->getReservoirData(); m_wellName = ""; m_x = nmDataAttribute("X", 0.0, "m", UNIT_TYPE_LENGTH, QStringList(), QStringList() << "m" << "cm" << "mm" << "in" << "0.1 in" << "ft" << "mile" << "km"); m_y = nmDataAttribute("Y", 0.0, "m", UNIT_TYPE_LENGTH, QStringList(), QStringList() << "m" << "cm" << "mm" << "in" << "0.1 in" << "ft" << "mile" << "km"); m_radius = nmDataAttribute("Radius", 0.0, "m", UNIT_TYPE_LENGTH, QStringList(), QStringList() << "m" << "cm" << "mm" << "in" << "0.1 in" << "ft" << "mile" << "km"); m_drillFloorElevation = nmDataAttribute("Drill floor elevation", 0.0, "m", UNIT_TYPE_LENGTH, QStringList(), QStringList() << "m" << "cm" << "mm" << "in" << "0.1 in" << "ft" << "mile" << "km"); m_zw = nmDataAttribute("Zw", 22.5, "m", UNIT_TYPE_LENGTH, QStringList(), QStringList() << "m" << "cm" << "mm" << "in" << "0.1 in" << "ft" << "mile" << "km"); - m_wellLength = nmDataAttribute("Well length", m_pReservoir->getThickness().getValue(), "m", UNIT_TYPE_LENGTH, QStringList(), QStringList() << "m" << "cm" << "mm" << "in" << "0.1 in" << "ft" << "mile" << "km"); + // 候选加载阶段尚未发布储层对象,井长随后会由 JSON 完整恢复。 + const QVariant oInitialWellLength = m_pReservoir == nullptr + ? QVariant(0.0) : m_pReservoir->getThickness().getValue(); + m_wellLength = nmDataAttribute("Well length", oInitialWellLength, "m", UNIT_TYPE_LENGTH, QStringList(), QStringList() << "m" << "cm" << "mm" << "in" << "0.1 in" << "ft" << "mile" << "km"); m_rateDependentSkin = nmDataAttribute("Rate dependent skin", false, ""); m_dSdQ = nmDataAttribute("dS/dQ", 0.0, "1/B/D", UNIT_TYPE_FLOW_RATE_RECIPROCAL, QStringList(), QStringList() << "1/B/D" << "1/MMm^3/D" << "1/Mcf/D" << "1/Mm^3/D" << "1/Mm^3/hr" @@ -179,6 +186,13 @@ rapidjson::Value nmDataWellBase::ToJsonValue(rapidjson::Document::AllocatorType& // 序列化井编码 wellObject.AddMember("WellCode", rapidjson::Value(m_wellCode.toStdString().c_str(), allocator).Move(), allocator); + // UUID 是实时井、井历史和结果快照之间唯一允许的关联键。 + const QByteArray baWellInstanceId = m_sWellInstanceId.toUtf8(); + wellObject.AddMember("WellInstanceId", + rapidjson::Value(baWellInstanceId.constData(), + static_cast(baWellInstanceId.size()), + allocator).Move(), allocator); + // 序列化井类型 wellObject.AddMember("WellType", rapidjson::Value(static_cast(m_eWellType)), allocator); @@ -247,6 +261,13 @@ void nmDataWellBase::FromJsonValue(const rapidjson::Value& jsonValue) m_wellCode = QString::fromUtf8(jsonValue["WellCode"].GetString()); } + // v3 已在 Manager 中完成格式校验,这里恢复求解时保存的稳定井身份。 + if(jsonValue.HasMember("WellInstanceId") && + jsonValue["WellInstanceId"].IsString()) { + restoreWellInstanceId(QString::fromUtf8( + jsonValue["WellInstanceId"].GetString())); + } + // 反序列化井类型 (m_eWellType) if(jsonValue.HasMember("WellType") && jsonValue["WellType"].IsInt()) { m_eWellType = static_cast(jsonValue["WellType"].GetInt()); diff --git a/Src/nmNum/nmData/nmNumericalSaveSession.cpp b/Src/nmNum/nmData/nmNumericalSaveSession.cpp index ddba41be..86b90f3f 100644 --- a/Src/nmNum/nmData/nmNumericalSaveSession.cpp +++ b/Src/nmNum/nmData/nmNumericalSaveSession.cpp @@ -209,7 +209,17 @@ bool loadManifest(const QString& sResultRootDirectory, setReferencedPaths.insert(oWindow.m_oMainJson.m_sRelativePath); mapWindows.insert(oWindow.m_sWindowId, oWindow); } - return !mapWindows.isEmpty() && bReferencesCurrentGeneration + // 删除全部数值分析窗口时允许发布空清单;它明确表示本成果当前没有数值载荷。 + if(mapWindows.isEmpty()) + { + const QString sGenerationDirectory = QDir( + sResultRootDirectory).filePath( + "NumericalGenerations/" + sGenerationId); + return QFileInfo(sGenerationDirectory).isDir() + ? true + : setError(pError, "Empty numerical manifest generation is missing."); + } + return bReferencesCurrentGeneration ? true : setError(pError, "Numerical root manifest does not reference its current generation."); } @@ -408,9 +418,9 @@ bool nmNumericalSaveSession::commit(QString* pError) { pError->clear(); } - if(!m_bActive || m_mapWindows.isEmpty()) + if(!m_bActive) { - return setError(pError, "Numerical save session has no window generation."); + return setError(pError, "Numerical save session is not active."); } rapidjson::Document oDocument; @@ -504,6 +514,8 @@ bool nmNumericalSaveSession::commit(QString* pError) m_bActive = false; // 只清理新清单未引用的代次;文件占用导致失败不影响已发布清单。 + // 空窗口清单仍保留一个空代次目录,供根清单发布后完整性复核。 + setReferencedGenerations.insert(m_sGenerationId); oIt = m_mapWindows.constBegin(); for(; oIt != m_mapWindows.constEnd(); ++oIt) { @@ -592,6 +604,33 @@ bool nmNumericalSaveSession::resolveWindowMainJson( return true; } +bool nmNumericalSaveSession::containsPublishedWindow( + const QString& sResultRootDirectory, + const QString& sWindowId, + bool& bContainsWindow, + QString* pError) +{ + bContainsWindow = false; + QString sNormalizedWindowId; + if(!nmNumericalResultPersistence::normalizeRelativePath( + sWindowId, sNormalizedWindowId) || + sNormalizedWindowId != sWindowId || + sNormalizedWindowId.contains('/')) + { + return setError(pError, "Numerical window identifier is invalid."); + } + + QMap mapWindows; + if(!loadManifest(sResultRootDirectory, mapWindows, NULL, pError)) + { + return false; + } + + // 空清单或未引用当前窗口表示该分析没有数值成果,不属于加载错误。 + bContainsWindow = mapWindows.contains(sWindowId); + return true; +} + bool nmNumericalSaveSession::validatePublishedManifest( const QString& sResultRootDirectory, QString* pError) diff --git a/Src/nmNum/nmPlot/nmPlotGraphicBinder.cpp b/Src/nmNum/nmPlot/nmPlotGraphicBinder.cpp index a79e2056..5fbeae32 100644 --- a/Src/nmNum/nmPlot/nmPlotGraphicBinder.cpp +++ b/Src/nmNum/nmPlot/nmPlotGraphicBinder.cpp @@ -688,6 +688,11 @@ void nmPlotGraphicBinder::bindOutlineToGraphic(nmObjBase* pGraphic, nmDataOutlin pGraphic->setName(pData->getName()); _pushOutlineDataToGraphic(pGraphic, pData); + if (!pData->hasVisualProps()) { + // 未打开Map就保存的边界没有有效视觉属性。保留新图元的默认样式, + // 并立即回填数据,保证下一次保存得到可重复恢复的有效样式。 + syncOutlineVisual(pGraphic); + } _connectOutline(pGraphic, pData); } diff --git a/Src/nmNum/nmSubWnd/nmSubWndUtils.cpp b/Src/nmNum/nmSubWnd/nmSubWndUtils.cpp index 1e881119..70f70dde 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndUtils.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndUtils.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "zxSysUtils.h" #include "ZxUiBase.h" @@ -792,24 +793,22 @@ bool nmSubWndUtils::beginNumericalSaveSession( if(pError != NULL) { pError->clear(); } - iSubWndFitting* pSubWndF = dynamic_cast(pFirstSubWnd); - nmDataAnalyzeManager* pDataManager = pSubWndF == nullptr ? nullptr : - nmDataAnalyzeManager::findManagerByFitting(pSubWndF); - nmDataAnalyzeContextProvider* pProvider = nmDataAnalyzeContext::provider(); - QString sLogicalWindowDirectory; + ZxDataWell* pDataWell = pFirstSubWnd == nullptr + ? nullptr : pFirstSubWnd->getDataWell(); nmSubWndUtils* pUtils = getInstance(); - if(pDataManager == nullptr || pProvider == nullptr || pUtils == nullptr || - pUtils->m_pNumericalSaveSession == nullptr || - !pProvider->getSaveResultDir(pSubWndF, sRstName, - sLogicalWindowDirectory)) { + if(pDataWell == nullptr || pUtils == nullptr || + pUtils->m_pNumericalSaveSession == nullptr) { if(pError != NULL) *pError = "Cannot resolve numerical save session root."; return false; } - const QFileInfo oWindowInfo(QDir::cleanPath(sLogicalWindowDirectory)); + QString sResultRootDirectory = ZxBaseUtil::getWellDirOf( + pDataWell->getName(), "Nm"); + sResultRootDirectory += sRstName; // 显式会话从空窗口清单开始,只有本轮实际遍历到的窗口会被发布。 return pUtils->m_pNumericalSaveSession->begin( - oWindowInfo.dir().absolutePath(), sRstName, false, pError); + QDir::cleanPath(sResultRootDirectory), + sRstName, false, pError); } bool nmSubWndUtils::saveWindowGeneration( @@ -890,63 +889,78 @@ bool nmSubWndUtils::saveRsts(iSubWnd* pSubWnd, \ return false; } - // 流动段分析窗体 - if(pSubWnd->getWndID() == "3004") { - // 转换为Fitting窗口 - iSubWndFitting* pSubWndF = dynamic_cast(pSubWnd); - if(pSubWndF == nullptr) { - return false; - } - // 根据窗体指针,找到创建的对应的数据管理对象 - nmDataAnalyzeManager* pDataManager = nmDataAnalyzeManager::findManagerByFitting(pSubWndF); + const QString sWindowId = pSubWnd->getWndID(); + if(sWindowId != "3004" && sWindowId != "5118" && + sWindowId != "5114") { + return true; + } - // 如果不存在,则说明当前分析窗体没有做数值解,不需要做保存处理 - if(pDataManager == nullptr) { - return false; - } + ZxMainWindow* pMainWnd1 = const_cast(pMainWnd); + ZxTabWidget* pTabWx = pSubWnd->getTabWx(); + nmSubWndUtils* pUtils = nmSubWndUtils::getInstance(); + if(pMainWnd1 == nullptr || pTabWx == nullptr || pUtils == nullptr) { + return false; + } - // 外层已经显式 begin 时只写当前窗口,由外层在遍历结束后 commit。 - // 尚未接入外层钩子的旧调用走单窗口会话,并保留清单中的其他窗口。 - nmSubWndUtils* pUtils = nmSubWndUtils::getInstance(); - QString sError; - if(pUtils != nullptr && pUtils->m_pNumericalSaveSession != nullptr && - pUtils->m_pNumericalSaveSession->isActive()) { - if(!saveWindowGeneration(pSubWnd, sRstName, &sError)) { - qWarning() << sError; - return false; - } - } else { - nmDataAnalyzeContextProvider* pProvider = nmDataAnalyzeContext::provider(); - QString sLogicalWindowDirectory; - if(pUtils == nullptr || pUtils->m_pNumericalSaveSession == nullptr || - pProvider == nullptr || - !pProvider->getSaveResultDir(pSubWndF, sRstName, - sLogicalWindowDirectory)) { - return false; - } - const QFileInfo oWindowInfo(QDir::cleanPath( - sLogicalWindowDirectory)); - if(!pUtils->m_pNumericalSaveSession->begin( - oWindowInfo.dir().absolutePath(), sRstName, - true, &sError) || - !pUtils->m_pNumericalSaveSession->saveWindowGeneration( - oWindowInfo.fileName(), pDataManager, &sError) || - !pUtils->m_pNumericalSaveSession->commit(&sError)) { - pUtils->m_pNumericalSaveSession->abort(); - qWarning() << sError; - return false; - } - } - } else if(pSubWnd->getWndID() == "5118") { - return true; - } else if(pSubWnd->getWndID() == "5114") { + // 外层框架仍按 develop 的方式逐窗口回调。数值代次必须以实际分析窗口 + // 作为保存锚点,不能让先到达的 Map 或网格回调改变成果井和根目录。 + QVector vecSubWnds = pMainWnd1->getAllSubWndsOf( + pTabWx, "3004", true); + iSubWnd* pSessionAnchor = vecSubWnds.isEmpty() + ? pSubWnd : vecSubWnds.first(); + ZxDataWell* pDataWell = pSessionAnchor == nullptr + ? nullptr : pSessionAnchor->getDataWell(); + if(pDataWell == nullptr) { + return false; + } + + QString sResultRootDirectory = ZxBaseUtil::getWellDirOf( + pDataWell->getName(), "Nm"); + sResultRootDirectory = QDir::cleanPath( + sResultRootDirectory + sRstName); + if(pUtils->m_setCompletedNumericalSaveRoots.contains( + sResultRootDirectory)) { return true; } - // TODO:Map窗口保存、网格窗口保存 + // 第一次回调在本模块内收齐当前 Tab 的完整分析窗口集合并整体发布。 + QString sError; + if(!beginNumericalSaveSession(pSessionAnchor, sRstName, &sError)) { + qWarning() << sError; + return false; + } + nmNumericalSaveSessionAbortGuard oAbortGuard; + for(int nIndex = 0; nIndex < vecSubWnds.size(); ++nIndex) { + iSubWndFitting* pSubWndF = + dynamic_cast(vecSubWnds[nIndex]); + if(pSubWndF == nullptr || + nmDataAnalyzeManager::findManagerByFitting(pSubWndF) == nullptr) { + continue; + } + if(!saveWindowGeneration(pSubWndF, sRstName, &sError)) { + qWarning() << sError; + return false; + } + } + if(!commitNumericalSaveSession(&sError)) { + qWarning() << sError; + return false; + } + oAbortGuard.release(); + + // 同一次外层遍历后续还会回调其他窗口,下一轮事件前不重复生成代次。 + pUtils->m_setCompletedNumericalSaveRoots.insert( + sResultRootDirectory); + QTimer::singleShot(0, pUtils, + SLOT(slotClearCompletedNumericalSaveRoots())); return true; } +void nmSubWndUtils::slotClearCompletedNumericalSaveRoots() +{ + m_setCompletedNumericalSaveRoots.clear(); +} + bool nmSubWndUtils::loadRsts(iSubWnd* pSubWnd, \ QString sRstName, \ const ZxMainWindow* pMainWnd) @@ -1009,6 +1023,24 @@ bool nmSubWndUtils::loadRsts(iSubWnd* pSubWnd, \ return false; } + bool bManifestContainsWindow = false; + QString sManifestError; + if(!nmNumericalSaveSession::containsPublishedWindow( + oLogicalWindowInfo.dir().absolutePath(), + sRstUtilWndCode, bManifestContainsWindow, + &sManifestError)) { + qWarning() << sManifestError; + QMessageBox::warning(nullptr, + tr("Numerical Result Load Failed"), + tr("The numerical result is incomplete or inconsistent and could not be loaded.")); + return false; + } + if(!bManifestContainsWindow) { + // 与 develop 的“目录不存在”语义一致:当前窗口没有数值成果, + // 不创建空 Manager,也不尝试从其他窗口或目录扫描推断数据。 + return true; + } + // 存在,说明保存了数值解的参数,需要解析文件里的数据到数据中心 // 动态创建数据中心类 nmDataAnalyzeManager* pDataManager = nmDataAnalyzeManager::getInstanceByFitting(pSubWndF); @@ -1022,10 +1054,11 @@ bool nmSubWndUtils::loadRsts(iSubWnd* pSubWnd, \ // 设置当前操作的是哪一个分析 nmDataAnalyzeManager::setCurrentFitting(pSubWndF); - // 加载当前分析中的PVT数据 - pDataManager->initPvtParaFromSubFit(); - // 加载本地文件的数据到数据中心里的数据体里(含用户选择的模型类型,覆盖PVT推导结果) + // v3 在候选 Manager 中读取 PVT 和全部成果,成功后才替换当前状态。 if(!pDataManager->loadNmResult(sDir)) { + QMessageBox::warning(nullptr, + tr("Numerical Result Load Failed"), + tr("The numerical result is incomplete or inconsistent and could not be loaded.")); return false; } // 注册井参数到 nmAttrRegistry(必须在 loadNmResult 之后,面板构建之前) @@ -1042,8 +1075,9 @@ bool nmSubWndUtils::loadRsts(iSubWnd* pSubWnd, \ return false; } - if(pWxDockNm1->widget() != nullptr && pWxDockNm2->widget() != nullptr) { - return true; //已经设定的话,则不再处理 + if(pWxDockNm1->widget() != nullptr && + pWxDockNm2->widget() != nullptr) { + return true; // 与 develop 一致,已有数值面板不重复创建。 } // 创建Map,暂时