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++

#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);
}