#include "nmWxPointerPosDlg.h" #include "iUnitWxBinder.h" #include "nmDataAnalyzeManager.h" #include "nmDataGeoRef.h" #include "nmGuiPlot.h" #include "ZxBaseUtil.h" #include #include #include #include #include #include #include #include #include #include #include namespace { // 与比例尺、测量工具保持相同的三列布局,不固定窗口或控件的像素尺寸。 QGridLayout* createCoordinateGrid(QGroupBox* pGroup) { pGroup->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); QGridLayout* pGrid = new QGridLayout(pGroup); pGrid->setContentsMargins(8, 15, 8, 10); pGrid->setHorizontalSpacing(6); pGrid->setVerticalSpacing(6); pGrid->setColumnStretch(0, 0); pGrid->setColumnStretch(1, 8); pGrid->setColumnStretch(2, 5); return pGrid; } // 只读而非禁用,保留鼠标选择、键盘导航和复制能力;纯文本字段可跨两列显示。 QLineEdit* createReadOnlyField(QGridLayout* pGrid, int nRow, const QString& sLabel, const QString& sObjectName, int nColumnSpan) { QWidget* pParent = pGrid->parentWidget(); QLabel* pLabel = new QLabel(sLabel, pParent); pLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); pLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); pGrid->addWidget(pLabel, nRow, 0); QLineEdit* pEdit = new QLineEdit(pParent); pEdit->setObjectName(sObjectName); pEdit->setReadOnly(true); pEdit->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); pEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); pGrid->addWidget(pEdit, nRow, 1, 1, nColumnSpan); return pEdit; } } nmWxPointerPosDlg::nmWxPointerPosDlg(QWidget* parent) : iDlgBase(parent), m_pLonEdit(nullptr), m_pLatEdit(nullptr), m_pUtmZoneEdit(nullptr) { for (int nField = 0; nField < LengthFieldCount; ++nField) { m_pLengthEdits[nField] = nullptr; m_pLengthUnits[nField] = nullptr; m_bUnitsBound[nField] = false; } // 浮动窗口的坐标与参考点必须来自同一画布,不能随全局当前分析切换而串用。 nmGuiPlot* pOwnerPlot = qobject_cast(parent); if (pOwnerPlot != nullptr) { m_pDataManager = pOwnerPlot->boundDataManager(); } initUI(); // 沿用原窗口的初始状态:尚未收到鼠标位置时,展示本地原点及对应参考点。 setPointerPos(0.0, 0.0); setWindowTitle(tr("Pointer position")); setWindowFlags((windowFlags() | Qt::WindowStaysOnTopHint) & ~Qt::WindowContextHelpButtonHint); // 初始大小由布局、字体和主题决定,允许用户随后调整窗口。 adjustSize(); } void nmWxPointerPosDlg::initUI() { QVBoxLayout* pMainLayout = new QVBoxLayout(this); pMainLayout->setContentsMargins(12, 10, 12, 0); pMainLayout->setSpacing(10); QGroupBox* pLocalGroup = new QGroupBox(tr("Local grid"), this); QGridLayout* pLocalGrid = createCoordinateGrid(pLocalGroup); createLengthField(pLocalGrid, 0, LocalX, tr("X"), "pointerX"); createLengthField(pLocalGrid, 1, LocalY, tr("Y"), "pointerY"); pMainLayout->addWidget(pLocalGroup); QGroupBox* pGeoGroup = new QGroupBox(tr("Coordinates"), this); QGridLayout* pGeoGrid = createCoordinateGrid(pGeoGroup); m_pLonEdit = createReadOnlyField(pGeoGrid, 0, tr("Longitude"), "pointerLongitude", 2); m_pLatEdit = createReadOnlyField(pGeoGrid, 1, tr("Latitude"), "pointerLatitude", 2); // UTM 带是参考点的只读属性,无需创建包含全部分带选项的禁用下拉框。 m_pUtmZoneEdit = createReadOnlyField(pGeoGrid, 2, tr("UTM zone"), "pointerUtmZone", 2); createLengthField(pGeoGrid, 3, Easting, tr("Easting"), "pointerEasting"); createLengthField(pGeoGrid, 4, Northing, tr("Northing"), "pointerNorthing"); pMainLayout->addWidget(pGeoGroup); // 两个分组按当前标签的推荐宽度对齐,不写死中文字宽或控件宽度。 int nLabelWidth = 0; const QList listLabels = findChildren(); for (int nIndex = 0; nIndex < listLabels.size(); ++nIndex) { nLabelWidth = qMax(nLabelWidth, listLabels[nIndex]->sizeHint().width()); } pLocalGrid->setColumnMinimumWidth(0, nLabelWidth); pGeoGrid->setColumnMinimumWidth(0, nLabelWidth); // 纵向余量留在内容与按钮之间,输入框和分组不随窗口拉高而变高。 pMainLayout->addStretch(1); QWidget* pFooter = new QWidget(this); pFooter->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); QHBoxLayout* pFooterLayout = new QHBoxLayout(pFooter); pFooterLayout->setContentsMargins(4, 5, 4, 5); pFooterLayout->setSpacing(8); pFooterLayout->addStretch(); QDialogButtonBox* pButtons = new QDialogButtonBox( QDialogButtonBox::Close, Qt::Horizontal, pFooter); QPushButton* pClose = pButtons->button(QDialogButtonBox::Close); pClose->setText(tr("Close")); pClose->setIcon(zxLoadIcon("Cancel")); pClose->setAutoDefault(false); pFooterLayout->addWidget(pButtons); pMainLayout->addWidget(pFooter); connect(pButtons, SIGNAL(rejected()), this, SLOT(reject())); } void nmWxPointerPosDlg::createLengthField(QGridLayout* pGrid, int nRow, LengthField field, const QString& sLabel, const QString& sObjectName) { QLineEdit* pEdit = createReadOnlyField(pGrid, nRow, sLabel, sObjectName, 1); QComboBox* pUnit = new QComboBox(pGrid->parentWidget()); pUnit->setObjectName(sObjectName + "Unit"); pUnit->setSizeAdjustPolicy(QComboBox::AdjustToContents); pUnit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); pGrid->addWidget(pUnit, nRow, 2); // 空参数标识配合 m 识别长度单位组,避免借用井参数而附带不适用的数值范围。 pUnit->addItem("m"); m_pLengthEdits[field] = pEdit; m_pLengthUnits[field] = pUnit; m_bUnitsBound[field] = bindUnitPairWxs(pEdit, pUnit, QString(), false); m_bUnitsBound[field] = m_bUnitsBound[field] && m_pUnitBinder != nullptr; // 初次有效值到达前保持空白;绑定失败时也不显示未经换算的坐标。 setLengthValue(field, 0.0, false); } void nmWxPointerPosDlg::setLengthValue(LengthField field, double dValue, bool bValid) { const bool bCanDisplay = bValid && qIsFinite(dValue) && m_bUnitsBound[field]; m_pLengthUnits[field]->setEnabled(bCanDisplay); if (m_bUnitsBound[field]) { // 只更新米制基准值,不重建单位列表,鼠标移动后仍保留用户的显示单位。 // 无数据时同步清除绑定器的旧值,避免后续单位切换使过期坐标重新出现。 m_pUnitBinder->setBaseValue(m_pLengthEdits[field], bCanDisplay ? dValue : 0.0); } if (!bCanDisplay) { m_pLengthEdits[field]->clear(); } } void nmWxPointerPosDlg::clearGeographicValues() { m_pLonEdit->clear(); m_pLatEdit->clear(); m_pUtmZoneEdit->clear(); setLengthValue(Easting, 0.0, false); setLengthValue(Northing, 0.0, false); } void nmWxPointerPosDlg::setPointerPos(double x, double y) { // 若所属分析已释放或画布已改绑,不把新画布的坐标套用到旧分析的参考点。 nmGuiPlot* pOwnerPlot = qobject_cast(parentWidget()); const bool bValidPoint = !m_pDataManager.isNull() && pOwnerPlot != nullptr && pOwnerPlot->boundDataManager() == m_pDataManager.data() && qIsFinite(x) && qIsFinite(y); setLengthValue(LocalX, x, bValidPoint); setLengthValue(LocalY, y, bValidPoint); if (!bValidPoint) { clearGeographicValues(); return; } // 每次从所属分析取最新对象,覆盖打开窗口后才设置参考点、参考点重建等情况。 nmDataGeoRef* pGeoRef = m_pDataManager->getGeoRefData(); if (pGeoRef == nullptr) { clearGeographicValues(); return; } const QString sZone = pGeoRef->getUtmZone().getValue().toString().trimmed(); bool bZoneOK = false; const int nZone = sZone.left(2).toInt(&bZoneOK); const QString sBands("CDEFGHJKLMNPQRSTUVWX"); if (!bZoneOK || nZone < 1 || nZone > 60 || sZone.size() != 3 || !sBands.contains(sZone.at(2))) { clearGeographicValues(); return; } // 空字段不能按 0 参与换算;无效数据只清空显示,不弹校验提示。 bool bEastingOK = false; bool bNorthingOK = false; bool bLongitudeOK = false; bool bLatitudeOK = false; const double dBaseEasting = pGeoRef->getEasting().getValue().toDouble(&bEastingOK); const double dBaseNorthing = pGeoRef->getNorthing().getValue().toDouble(&bNorthingOK); const double dBaseLongitude = pGeoRef->getLongitude().getValue().toDouble(&bLongitudeOK); const double dBaseLatitude = pGeoRef->getLatitude().getValue().toDouble(&bLatitudeOK); if (!bEastingOK || !bNorthingOK || !bLongitudeOK || !bLatitudeOK || !qIsFinite(dBaseEasting) || !qIsFinite(dBaseNorthing) || !qIsFinite(dBaseLongitude) || !qIsFinite(dBaseLatitude) || qAbs(dBaseLongitude) > 180.0 || qAbs(dBaseLatitude) > 90.0) { clearGeographicValues(); return; } // 模型坐标为相对参考点的米制偏移,显示单位不参与坐标计算。 const double dEasting = dBaseEasting + x; const double dNorthing = dBaseNorthing + y; if (!qIsFinite(dEasting) || !qIsFinite(dNorthing)) { clearGeographicValues(); return; } m_pUtmZoneEdit->setText(sZone); setLengthValue(Easting, dEasting, true); setLengthValue(Northing, dNorthing, true); // 沿用现有的小范围经纬度近似换算,并非严格的 UTM 反投影。 // 极点附近经度换算分母趋近于零,此时保留平面坐标,清空无法可靠计算的经纬度。 const double dMetersPerDegreeLat = 111111.0; const double dPi = 3.14159265358979323846; const double dMetersPerDegreeLon = dMetersPerDegreeLat * std::cos(dBaseLatitude * dPi / 180.0); if (qAbs(dMetersPerDegreeLon) < 1.0e-6) { m_pLonEdit->clear(); m_pLatEdit->clear(); return; } const double dLatitude = dBaseLatitude + y / dMetersPerDegreeLat; const double dLongitude = dBaseLongitude + x / dMetersPerDegreeLon; m_pLonEdit->setText(formatCoordinate(dLongitude, false)); m_pLatEdit->setText(formatCoordinate(dLatitude, true)); } QString nmWxPointerPosDlg::formatCoordinate(double dCoordinate, bool bLatitude) const { if (!qIsFinite(dCoordinate) || qAbs(dCoordinate) > (bLatitude ? 90.0 : 180.0)) { return QString(); } // 先按百分之一秒整体取整再分解,自动处理 59.995 秒进位,避免出现 60 秒。 // 经纬度范围已限定,最大值为 180 * 3600 * 100,不会超出 int 范围。 const int nTotalHundredths = qRound(qAbs(dCoordinate) * 360000.0); const int nDegrees = nTotalHundredths / 360000; const int nMinutes = (nTotalHundredths / 6000) % 60; const double dSeconds = (nTotalHundredths % 6000) / 100.0; const QString sDirection = bLatitude ? (dCoordinate >= 0.0 ? "N" : "S") : (dCoordinate >= 0.0 ? "E" : "W"); return QString("%1%2%3%4%5%6%7") .arg(nDegrees, 2, 10, QLatin1Char('0')).arg(tr("deg")) .arg(nMinutes, 2, 10, QLatin1Char('0')).arg(tr("min")) .arg(dSeconds, 5, 'f', 2, QLatin1Char('0')).arg(tr("sec")).arg(sDirection); }