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