You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nmWTAI-Platform/Src/nmNum/nmSubWxs/nmWxPointerPosDlg.cpp

288 lines
12 KiB
C++

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#include "nmWxPointerPosDlg.h"
#include "iUnitWxBinder.h"
#include "nmDataAnalyzeManager.h"
#include "nmDataGeoRef.h"
#include "nmGuiPlot.h"
#include "ZxBaseUtil.h"
#include <QComboBox>
#include <QDialogButtonBox>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QVBoxLayout>
#include <qnumeric.h>
#include <cmath>
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<nmGuiPlot*>(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<QLabel*> listLabels = findChildren<QLabel*>();
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<nmGuiPlot*>(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);
}