From aabbf0bc729bf3a2f3a92bbe8ba2620dcc08993c Mon Sep 17 00:00:00 2001 From: lh <2334563547@qq.com> Date: Fri, 13 Nov 2026 14:11:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(Nm):=20=E7=BB=9F=E4=B8=80=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=E6=98=BE=E7=A4=BA=E7=95=8C=E9=9D=A2=E5=B9=B6?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8F=82=E8=80=83=E7=82=B9=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 统一只读坐标输入框和底部按钮样式,标签去除冒号 - 接入框架单位换算,鼠标移动后保留所选显示单位 - 输入框与单位框按 8:5 横向伸缩,取消窗口固定尺寸 - 绑定所属分析,动态读取参考点,避免数据串用及失效指针访问 - 完善无效坐标处理和度分秒显示,补充中文注释及翻译 --- Bin/Config/Lang/cn/nmNum_cn.qm | Bin 207835 -> 207822 bytes Bin/Config/Lang/cn/nmNum_cn.ts | 28 +- Include/nmNum/nmSubWxs/nmWxPointerPosDlg.h | 75 ++- Src/nmNum/nmSubWnd/nmSubWndMain.cpp | 3 +- Src/nmNum/nmSubWxs/nmWxPointerPosDlg.cpp | 508 +++++++++++---------- 5 files changed, 326 insertions(+), 288 deletions(-) diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm index 140921bd0b5ed5220924ec548aeb2a5650122884..37718f1e08fa6cc00ae423236abfac391122020e 100644 GIT binary patch delta 4221 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 delta 4172 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 本地网格 - X: - + X + X - Y: - + Y + Y Coordinates 坐标 - Longitude: - 经度: + Longitude + 经度 - Latitude: - 纬度: + Latitude + 纬度 - UTM zone: - UTM 带: + UTM zone + UTM 带 - Easting: - 东向坐标: + Easting + 东向坐标 - Northing: - 北向坐标: + Northing + 北向坐标 Close diff --git a/Include/nmNum/nmSubWxs/nmWxPointerPosDlg.h b/Include/nmNum/nmSubWxs/nmWxPointerPosDlg.h index d8366f49..8756d787 100644 --- a/Include/nmNum/nmSubWxs/nmWxPointerPosDlg.h +++ b/Include/nmNum/nmSubWxs/nmWxPointerPosDlg.h @@ -4,45 +4,72 @@ #include "iDlgBase.h" #include "nmSubWxs_global.h" -#include "nmDataGeoRef.h" +#include -class QLineEdit; class QComboBox; -class QPushButton; -class nmGUIComponentLineEdit; -class nmDataAttribute; +class QGridLayout; +class QLineEdit; +class nmDataAnalyzeManager; +/** + * @brief 模型位置显示窗口,跟随所属画布的鼠标位置更新坐标,不修改分析数据。 + * @note 本地 X/Y 和东向、北向坐标以米接收,由框架单位绑定器负责显示换算。 + * 坐标均可选中复制;UTM 带只展示当前参考点的设置,不提供编辑入口。 + * 输入列与单位列按 8:5 横向伸展,纵向采用字体和主题的推荐高度。 + */ class NM_SUB_WXS_EXPORT nmWxPointerPosDlg : public iDlgBase { - Q_OBJECT + Q_OBJECT public: - explicit nmWxPointerPosDlg(QWidget* parent = 0); - - public slots: - void setPointerPos(double x, double y); + /** @brief 绑定父级 nmGuiPlot 的分析管理器,由 Qt 父子关系管理窗口生命周期。 */ + explicit nmWxPointerPosDlg(QWidget* parent = nullptr); +public slots: + /** @brief 接收画布的米制坐标,刷新显示并保留各字段已选择的单位。 */ + void setPointerPos(double x, double y); private: - void initUI(); - void loadGeoRefDataToUI(); - QString formatCoordinate(double coordinate, bool isLatitude); // ¾­γ¶Èת»» + /** @brief 四个长度字段分别保存单位选择,不混用本地坐标与地理坐标。 */ + enum LengthField + { + LocalX, + LocalY, + Easting, + Northing, + LengthFieldCount + }; + + /** @brief 创建本地网格、地理坐标分组及标准关闭按钮。 */ + void initUI() override; + /** @brief 添加只读长度字段,并通过初始单位 m 识别框架长度单位组。 */ + void createLengthField(QGridLayout* pGrid, int nRow, LengthField field, + const QString& sLabel, const QString& sObjectName); - nmGUIComponentLineEdit* m_pX; - nmGUIComponentLineEdit* m_pY; + /** @brief 回填米制值;无有效数据时清空显示并暂停单位切换,避免恢复旧值。 */ + void setLengthValue(LengthField field, double dValue, bool bValid); - nmDataAttribute* m_pAttrX; - nmDataAttribute* m_pAttrY; + /** @brief 未设置参考点或参考数据失效时,清空地理坐标,保留本地坐标显示。 */ + void clearGeographicValues(); + + /** @brief 将有限且在范围内的经纬度转换为度分秒,按百分之一秒四舍五入并进位。 */ + QString formatCoordinate(double dCoordinate, bool bLatitude) const; + +private: + // 借用所属分析的管理器,删除时自动置空,不使用全局当前分析。 + // 参考点可能在窗口打开后新增、清空或重建,每次刷新重新读取,不缓存其裸指针。 + QPointer m_pDataManager; - QLineEdit* m_pLonEdit; - QLineEdit* m_pLatEdit; - QComboBox* m_pUtmZoneCombo; - QLineEdit* m_pEastingEdit; - QLineEdit* m_pNorthingEdit; - QPushButton* m_pCloseButton; + // 长度控件均由 Qt 父子关系释放;绑定状态独立记录,禁止以错误单位展示原始值。 + QLineEdit* m_pLengthEdits[LengthFieldCount]; + QComboBox* m_pLengthUnits[LengthFieldCount]; + bool m_bUnitsBound[LengthFieldCount]; - nmDataGeoRef* m_pGeoRefData; + // 非长度字段使用普通只读文本框,经纬度文本本身包含度、分、秒符号。 + QLineEdit* m_pLonEdit; + QLineEdit* m_pLatEdit; + QLineEdit* m_pUtmZoneEdit; }; #endif // NMWXPOINTERPOSDLG_H diff --git a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp index ee94e63a..75ee1667 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp @@ -1756,7 +1756,8 @@ QWidget* nmSubWndMain::showPointerPosDialog() { nmGuiPlot* plot = dynamic_cast(m_pWxPlot); - if(plot == nullptr) { + // 位置窗口只服务于已绑定分析的画布,避免回退到全局当前分析。 + if(plot == nullptr || plot->boundDataManager() == nullptr) { return nullptr; } diff --git a/Src/nmNum/nmSubWxs/nmWxPointerPosDlg.cpp b/Src/nmNum/nmSubWxs/nmWxPointerPosDlg.cpp index 27f1b423..e69d522e 100644 --- a/Src/nmNum/nmSubWxs/nmWxPointerPosDlg.cpp +++ b/Src/nmNum/nmSubWxs/nmWxPointerPosDlg.cpp @@ -1,277 +1,287 @@ #include "nmWxPointerPosDlg.h" -#include "nmGUIComponentLineEdit.h" + +#include "iUnitWxBinder.h" #include "nmDataAnalyzeManager.h" -#include "nmDataAttribute.h" +#include "nmDataGeoRef.h" +#include "nmGuiPlot.h" +#include "ZxBaseUtil.h" -#include #include -#include +#include +#include #include -#include #include -#include #include +#include +#include +#include +#include #include -nmWxPointerPosDlg::nmWxPointerPosDlg(QWidget* parent) - : iDlgBase(parent) +namespace { - setWindowTitle(tr("Pointer position")); - - m_pAttrX = new nmDataAttribute(); - m_pAttrX->setName("X"); - m_pAttrX->setUnit("m"); - m_pAttrX->setListUnitSelections(QStringList() << "m" << "ft" << "cm"); - m_pAttrX->setValue(0.0); - - m_pAttrY = new nmDataAttribute(); - m_pAttrY->setName("Y"); - m_pAttrY->setUnit("m"); - m_pAttrY->setListUnitSelections(QStringList() << "m" << "ft" << "cm"); - m_pAttrY->setValue(0.0); + // 与比例尺、测量工具保持相同的三列布局,不固定窗口或控件的像素尺寸。 + 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; + } +} - m_pGeoRefData = nmDataAnalyzeManager::getCurrentInstance()->getGeoRefData(); +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(); - loadGeoRefDataToUI(); - this->resize(320, 400); - this->setFixedSize(this->size()); + // 沿用原窗口的初始状态:尚未收到鼠标位置时,展示本地原点及对应参考点。 + setPointerPos(0.0, 0.0); + setWindowTitle(tr("Pointer position")); + setWindowFlags((windowFlags() | Qt::WindowStaysOnTopHint) & + ~Qt::WindowContextHelpButtonHint); + // 初始大小由布局、字体和主题决定,允许用户随后调整窗口。 + adjustSize(); } void nmWxPointerPosDlg::initUI() { - // Local grid group - QGroupBox* localGroup = new QGroupBox(tr("Local grid")); - - QFormLayout* localForm = new QFormLayout; - localForm->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter); - localForm->setVerticalSpacing(10); - localForm->setHorizontalSpacing(15); - localForm->setContentsMargins(8, 8, 8, 8); - - m_pX = new nmGUIComponentLineEdit(m_pAttrX, false); - m_pY = new nmGUIComponentLineEdit(m_pAttrY, false); - QLineEdit* leX = m_pX->findChild(); - - if(leX) { - leX->setMinimumWidth(100); - } - - QLineEdit* leY = m_pY->findChild(); - - if(leY) { - leY->setMinimumWidth(100); - } - - QWidget* xWidget = new QWidget; - QHBoxLayout* xLay = new QHBoxLayout(xWidget); - xLay->setContentsMargins(0, 0, 0, 0); - xLay->addWidget(m_pX); - - QWidget* yWidget = new QWidget; - QHBoxLayout* yLay = new QHBoxLayout(yWidget); - yLay->setContentsMargins(0, 0, 0, 0); - yLay->addWidget(m_pY); - - localForm->addRow(tr("X:"), xWidget); - localForm->addRow(tr("Y:"), yWidget); - - QVBoxLayout* localWrapper = new QVBoxLayout; - localWrapper->addStretch(1); - localWrapper->addLayout(localForm); - localWrapper->addStretch(1); - localWrapper->setContentsMargins(0, 0, 0, 0); - localGroup->setLayout(localWrapper); - - // Coordinates group - QGroupBox* coordGroup = new QGroupBox(tr("Coordinates")); - coordGroup->setEnabled(true); - - QFormLayout* coordForm = new QFormLayout; - coordForm->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter); - coordForm->setVerticalSpacing(10); - coordForm->setHorizontalSpacing(15); - coordForm->setContentsMargins(8, 8, 8, 8); - - m_pLonEdit = new QLineEdit; - m_pLatEdit = new QLineEdit; - m_pUtmZoneCombo = new QComboBox; - m_pEastingEdit = new QLineEdit; - m_pNorthingEdit = new QLineEdit; - - // 初始化UTM Zone下拉框选项 - QStringList bands; - bands << "C" << "D" << "E" << "F" << "G" << "H" << "J" << "K" << "L" << "M" - << "N" << "P" << "Q" << "R" << "S" << "T" << "U" << "V" << "W" << "X"; - - // 外层循环:Band;内层循环:Zone - for(int i = 0; i < bands.size(); ++i) { - QString band = bands.at(i); - for(int zone = 1; zone <= 60; ++zone) { - QString zoneStr = QString("%1%2").arg(zone, 2, 10, QLatin1Char('0')).arg(band); - m_pUtmZoneCombo->addItem(zoneStr); - } - } - - m_pLonEdit->setEnabled(false); - m_pLatEdit->setEnabled(false); - m_pUtmZoneCombo->setEnabled(false); // ComboBox禁用编辑 - m_pEastingEdit->setEnabled(false); - m_pNorthingEdit->setEnabled(false); - - coordForm->addRow(tr("Longitude:"), m_pLonEdit); - coordForm->addRow(tr("Latitude:"), m_pLatEdit); - coordForm->addItem(new QSpacerItem(0, 20, QSizePolicy::Minimum, QSizePolicy::Fixed)); - coordForm->addRow(tr("UTM zone:"), m_pUtmZoneCombo); - coordForm->addRow(tr("Easting:"), m_pEastingEdit); - coordForm->addRow(tr("Northing:"), m_pNorthingEdit); - - QVBoxLayout* coordWrapper = new QVBoxLayout; - coordWrapper->addStretch(1); - coordWrapper->addLayout(coordForm); - coordWrapper->addStretch(1); - coordWrapper->setContentsMargins(0, 0, 0, 0); - coordGroup->setLayout(coordWrapper); - - // Bottom button - m_pCloseButton = new QPushButton(tr("Close")); - connect(m_pCloseButton, SIGNAL(clicked()), this, SLOT(reject())); - QHBoxLayout* btnLay = new QHBoxLayout; - btnLay->addStretch(); - btnLay->addWidget(m_pCloseButton); - - // 整体布局 - QVBoxLayout* mainLay = new QVBoxLayout; - mainLay->setContentsMargins(12, 12, 12, 12); - mainLay->setSpacing(15); - mainLay->addWidget(localGroup); - mainLay->addWidget(coordGroup); - mainLay->addLayout(btnLay); - setLayout(mainLay); + 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::setPointerPos(double x, double y) +void nmWxPointerPosDlg::createLengthField(QGridLayout* pGrid, int nRow, + LengthField field, const QString& sLabel, + const QString& sObjectName) { - // 更新X、Y坐标显示 - m_pX->setDisplayValue(x); - m_pY->setDisplayValue(y); - - // 检查是否有地理参考数据 - if (!m_pGeoRefData) { - return; // 提前返回,避免后续无效计算 - } - - // 验证地理参考数据有效性 - QString utmZone = m_pGeoRefData->getUtmZone().getValue().toString(); - if (utmZone.isEmpty()) { - return; // 提前返回,避免后续无效计算 - } - - // 获取基础地理参考坐标 - double baseEasting = m_pGeoRefData->getEasting().getValue().toDouble(); - double baseNorthing = m_pGeoRefData->getNorthing().getValue().toDouble(); - double baseLongitude = m_pGeoRefData->getLongitude().getValue().toDouble(); - double baseLatitude = m_pGeoRefData->getLatitude().getValue().toDouble(); - - // 计算并更新UTM坐标 - double newEasting = baseEasting + x; - double newNorthing = baseNorthing + y; - m_pEastingEdit->setText(QString::number(newEasting, 'f', 2)); - m_pNorthingEdit->setText(QString::number(newNorthing, 'f', 2)); - - // 计算并更新经纬度坐标 - const double METERS_PER_DEGREE_LAT = 111111.0; - const double PI = 3.14159265358979323846; - - double latitudeRadians = baseLatitude * PI / 180.0; - double metersPerDegreeLon = METERS_PER_DEGREE_LAT * cos(latitudeRadians); - - double newLatitude = baseLatitude + (y / METERS_PER_DEGREE_LAT); - double newLongitude = baseLongitude + (x / metersPerDegreeLon); - - // 转换为度分秒格式并更新UI - QString latStr = formatCoordinate(newLatitude, true); - QString lonStr = formatCoordinate(newLongitude, false); - m_pLatEdit->setText(latStr); - m_pLonEdit->setText(lonStr); + 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::loadGeoRefDataToUI() +void nmWxPointerPosDlg::setLengthValue(LengthField field, double dValue, bool bValid) { - if (!m_pGeoRefData) { - m_pUtmZoneCombo->setCurrentIndex(-1); // 设置为空选项 - return; - } - - // 检查是否有有效的地理参考数据 - QString utmZone = m_pGeoRefData->getUtmZone().getValue().toString(); - bool hasValidData = !utmZone.isEmpty(); - - if (hasValidData) { - // 加载经纬度 - double longitude = m_pGeoRefData->getLongitude().getValue().toDouble(); - double latitude = m_pGeoRefData->getLatitude().getValue().toDouble(); - - // 分别转换经度和纬度,并设置到UI - QString lonStr = formatCoordinate(longitude, false); // false表示经度 - QString latStr = formatCoordinate(latitude, true); // true表示纬度 - - m_pLonEdit->setText(lonStr); // 显示转换后的经度 - m_pLatEdit->setText(latStr); // 显示转换后的纬度 - - // 加载UTM Zone - int index = m_pUtmZoneCombo->findText(utmZone); - if (index >= 0) { - m_pUtmZoneCombo->setCurrentIndex(index); - } - - // 加载UTM坐标 - double easting = m_pGeoRefData->getEasting().getValue().toDouble(); - double northing = m_pGeoRefData->getNorthing().getValue().toDouble(); - - m_pEastingEdit->setText(QString::number(easting, 'f', 2)); - m_pNorthingEdit->setText(QString::number(northing, 'f', 2)); - } + 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(); + } } -QString nmWxPointerPosDlg::formatCoordinate(double coordinate, bool isLatitude) +void nmWxPointerPosDlg::clearGeographicValues() { - // 判断正负,确定方向 - bool isPositive = coordinate >= 0; - double absCoordinate = qAbs(coordinate); - - // 提取度数 - int degrees = static_cast(absCoordinate); - - // 计算分数 - double minutesFloat = (absCoordinate - degrees) * 60.0; - int minutes = static_cast(minutesFloat); - - // 计算秒数 - double seconds = (minutesFloat - minutes) * 60.0; - - // 防止浮点精度问题导致秒数≥60 - if (seconds >= 60.0) { - seconds = 59.999; - } - - // 确定方向字符 - QString direction; - if (isLatitude) { - direction = isPositive ? "N" : "S"; // 纬度 - } else { - direction = isPositive ? "E" : "W"; // 经度 - } + m_pLonEdit->clear(); + m_pLatEdit->clear(); + m_pUtmZoneEdit->clear(); + setLengthValue(Easting, 0.0, false); + setLengthValue(Northing, 0.0, false); +} - // 分步构建字符串 - QString result; - result += QString("%1").arg(degrees, 2, 10, QChar('0')); // 度数 - result += QString(tr("deg")); // 度符号 - result += QString("%1").arg(minutes, 2, 10, QChar('0')); // 分数 - result += QString(tr("min")); // 分符号 - result += QString("%1").arg(static_cast(seconds), 2, 10, QChar('0')); // 秒数 - result += QString(tr("sec")); // 秒符号 - result += direction; // 方向 +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)); +} - return result; -} \ No newline at end of file +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); +}