diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm index db6075c6..3463295b 100644 Binary files a/Bin/Config/Lang/cn/nmNum_cn.qm and b/Bin/Config/Lang/cn/nmNum_cn.qm differ diff --git a/Bin/Config/Lang/cn/nmNum_cn.ts b/Bin/Config/Lang/cn/nmNum_cn.ts index 5f468668..4daefb70 100644 --- a/Bin/Config/Lang/cn/nmNum_cn.ts +++ b/Bin/Config/Lang/cn/nmNum_cn.ts @@ -5822,32 +5822,28 @@ Please choose a smaller length. 位置 - Longitude: - 经度: + Longitude + 经度 - Latitude: - 纬度: + Latitude + 纬度 Zone 投影带 - UTM zone: - UTM 带: - - - Easting: - 东向坐标: + UTM zone + UTM 带 - Northing: - 北向坐标: + Easting + 东向坐标 - Google - Google + Northing + 北向坐标 OK @@ -5857,64 +5853,6 @@ Please choose a smaller length. Cancel 取消 - - Input Error - 输入错误 - - - Please enter valid numbers - 请输入有效的数字 - - - Latitude must be between - 90° and + 90° - 纬度必须在 -90° 到 +90° 之间 - - - Longitude must be between - 180° and + 180° - 经度必须在 -180° 到 +180° 之间 - - - Easting must be between 0 and 1,000,000 meters - 东向坐标必须在 0 到 1,000,000 米之间" - - - Northing must be between 0 and 10,000,000 meters - 北向坐标必须在 0 到 10,000,000 米之间 - - - Please select a valid UTM Zone - 请选择有效的UTM带 - - - Conversion Error - 转换错误 - - - UTM coordinate conversion result is out of valid range, please check input - UTM坐标转换结果超出有效范围,请检查输入 - - - Latitude must be between - 90o and + 90o - 纬度必须在-90°至90°之间 - - - Longitude must be between - 180o and + 180o - 经度必须在-180°至180°之间 - - - The UTM coordinates you entered correspond to an extreme polar region where coordinate conversion may be inaccurate. -Please try coordinates closer to the center of the UTM zone. - 您输入的UTM坐标对应极地区域,坐标转换可能不准确。 -请尝试使用更接近UTM区域中心的坐标。 - - - UTM coordinate conversion failed. -The coordinates may be outside the valid range for this UTM zone. -Please check your input coordinates. - UTM坐标转换失败。 -输入的坐标可能超出了该UTM区域的有效范围。 -请检查您的输入坐标。 - nmWxGeometryLayerDlg diff --git a/Include/nmNum/nmSubWxs/nmWxGeoRefDlg.h b/Include/nmNum/nmSubWxs/nmWxGeoRefDlg.h index 4241ca39..c168d734 100644 --- a/Include/nmNum/nmSubWxs/nmWxGeoRefDlg.h +++ b/Include/nmNum/nmSubWxs/nmWxGeoRefDlg.h @@ -2,70 +2,86 @@ #define NMWXGEOREFDLG_H #include "iDlgBase.h" -#include -#include -#include -#include - #include "nmSubWxs_global.h" #include "nmDataGeoRef.h" +class QComboBox; +class QGridLayout; +class QLineEdit; +class QPushButton; +class iUnitGroup; + +// 模型参考点编辑器:编辑本地原点对应的 WGS84 经纬度和 UTM 坐标。 +// 只持有数据副本,不读取全局当前分析;调用方在确定后负责写回所属分析,取消不改工程。 class NM_SUB_WXS_EXPORT nmWxGeoRefDlg : public iDlgBase { Q_OBJECT public: - explicit nmWxGeoRefDlg(QWidget* parent = 0); - void loadDataToUI(); // 从数据加载到界面 - void saveDataUI(); // 从界面保存到数据 - - // 坐标转换辅助函数 - void updateFromLatLon(); // 根据经纬度更新UTM坐标 - void updateFromUTM(); // 根据UTM坐标更新经纬度 - void latLonToUTM(double lat, double lon, int& zone, char& band, double& easting, double& northing); - bool utmToLatLon(int zone, char band, double easting, double northing, double& lat, double& lon); - int getUTMZoneFromLongitude(double longitude); - char getUTMBandFromLatitude(double latitude); - double calculateNorthing(double latitude, double longitude, int zone); - void restoreLastValidValues(); + // 复制传入的参考点并初始化界面;构造和加载均不修改调用方数据,不自动重算旧坐标。 + explicit nmWxGeoRefDlg(const nmDataGeoRef& oData, QWidget* pParent = 0); + // 返回窗口持有的结果副本,仅在 Accepted 后读取;引用有效期不超过窗口生命周期。 + const nmDataGeoRef& geoRefData() const; + // 判断是否发生坐标编辑;只打开窗口或切换显示单位返回 false,调用方可跳过写回。 + bool hasChanges() const; - // 输入验证函数 - bool isValidLatitude(double lat); - bool isValidLongitude(double lon); - bool isValidEasting(double easting); - bool isValidNorthing(double northing); +public slots: + // 确定按钮和回车共用提交入口;非法/不完整输入保留在界面中,不按零值保存。 + virtual void accept(); - private slots: - void onUtmZoneChanged(int index); - void onLongitudeChanged(); // 经度改变槽函数 - void onLatitudeChanged(); // 纬度改变槽函数 - void onEastingChanged(); // 东向坐标改变槽函数 - void onNorthingChanged(); // 北向坐标改变槽函数 - void okAccept(); - void cancelReject(); +private slots: + // 响应用户修改文本,按信号来源确定经纬度或投影坐标组,检查并更新另一组坐标。 + void onCoordinateEdited(const QString& sText); + // 响应长度单位选择,仅从米制原值刷新对应字段;无效输入或换算失败时恢复原单位。 + void onLengthUnitChanged(int nIndex); + // 响应 UTM 带选择,以该带覆盖区域中心初始化四个坐标;程序刷新期间不重复处理。 + void onUtmZoneChanged(int nIndex); private: - void initUI(); - - QLineEdit* m_pLongitudeEdit; - QLineEdit* m_pLatitudeEdit; + // 坐标字段及数组下标约定:前两项为度制经纬度,后两项为米制投影坐标。 + enum CoordinateField { Longitude = 0, Latitude, Easting, Northing, FieldCount }; + // 转换输入来源;NoInput 用于刚加载或选带初始化完成、不需要再次反算的状态。 + enum InputSource { NoInput = 0, GeographicInput, ProjectedInput }; - QComboBox* m_pUtmZoneCombo; - QLineEdit* m_pEastingEdit; - QLineEdit* m_pNorthingEdit; - - QPushButton* m_pOkButton; - QPushButton* m_pCancelButton; - - QString m_lastLongitude; - QString m_lastLatitude; - QString m_lastEasting; - QString m_lastNorthing; + // 创建两组坐标字段、框架长度单位下拉框及标准按钮栏,不固定窗口和字段宽度。 + void initUI(); + // 回显副本中的原始坐标并记录完整精度;空值保持为空,不因打开窗口触发投影或写回。 + void loadDataToUI(); + // 在指定网格行创建字段;经纬度跨两列,东向/北向字段附带框架长度单位选择。 + void createCoordinateField(QGridLayout* pGrid, int nRow, CoordinateField eField, + const QString& sLabel, const QString& sObjectName); + // 读取时优先保留未编辑字段的完整原值;长度文本通过框架单位组换算为米。 + bool readField(CoordinateField eField, double& dValue) const; + // 将度/米制原值格式化为显示文本;长度采用框架目标单位精度,换算失败返回空串。 + QString displayValue(CoordinateField eField, double dValue, const QString& sUnit) const; + // 经纬度/投影坐标成对转换,全部成功后才替换副本;失败保留输入,禁用确定。 + bool updateCoordinates(InputSource eSource); + // 一次性接收成功转换的完整坐标(度、米),保留输入组原文并刷新派生组和 UTM 带。 + void applyCoordinates(double dLongitude, double dLatitude, double dEasting, + double dNorthing, const QString& sZone, InputSource eSource); + // 按单位服务和输入有效性更新操作状态;无效输入阻止确定,并暂时锁住另一坐标组。 + void updateActions(); + // 检查框架长度单位组及米制入口是否可用;不可用时禁止坐标编辑和保存。 + bool unitsAvailable() const; - // 防止递归更新的标志 - bool m_bUpdating; +private: + nmDataGeoRef m_oGeoRefData; // 工程参考点的编辑副本,保留原属性及持久化格式。 + iUnitGroup* m_pLengthUnitGroup; // 借用框架长度单位组;单位选项和换算规则均由框架维护。 + double m_dBaseValues[FieldCount]; // 最近一组完整坐标:经纬度为度,东向/北向为米。 + QString m_sShownTexts[FieldCount]; // 对应原值的显示文本,用于避免舍入显示反写完整精度。 + QString m_sDisplayUnits[FieldCount]; // 当前显示单位;经纬度固定为度,长度允许分别切换。 + QString m_sUtmZone; // 最近一组完整坐标的分带标识,如 50S;字母表示纬度带。 + InputSource m_eLastInput; // 最近编辑的坐标组,确定时按该组再次检查转换结果。 + bool m_bHasCoordinates; // 是否已有一组完整坐标,可用于保留未编辑字段的原值。 + bool m_bInputValid; // 当前输入能否转换;无效时保留原文并阻止确定。 + bool m_bRefreshing; // 程序刷新控件期间抑制联动,避免递归转换。 + bool m_bChanged; // 只记录坐标编辑;打开窗口和切换单位不算修改。 - nmDataGeoRef m_geoRefData; + // 以下控件交给 Qt 父子关系释放;输入框和单位框横向伸展,高度采用主题建议值。 + QLineEdit* m_pCoordinateEdits[FieldCount];// 经纬度及投影坐标输入框,顺序对应 CoordinateField。 + QComboBox* m_pLengthUnits[FieldCount]; // 仅 Easting/Northing 使用,角度字段对应项为空。 + QComboBox* m_pUtmZoneComboBox; // UTM 带选择,沿用数据对象提供的分带选项。 + QPushButton* m_pOkButton; // 仅在单位服务和当前坐标均有效时允许提交。 }; #endif // NMWXGEOREFDLG_H diff --git a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp index 80e6ac40..bb2b8225 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp @@ -1646,8 +1646,14 @@ void nmSubWndMain::drawMeasure() void nmSubWndMain::drawGeoRef() { - nmWxGeoRefDlg dlg(nullptr); - dlg.exec(); + // 参考点属于当前地图绑定的分析;编辑器只收发副本,不访问全局当前分析。 + nmGuiPlot* pPlot = dynamic_cast(m_pWxPlot); + QPointer pManager = pPlot ? pPlot->boundDataManager() : nullptr; + if (!pManager) return; + nmWxGeoRefDlg dialog(pManager->getGeoRefDataCopy(), this); + if (dialog.exec() == QDialog::Accepted && pManager && dialog.hasChanges()) { + pManager->updateGeoRefData(dialog.geoRefData()); + } } void nmSubWndMain::drawPointerPos() diff --git a/Src/nmNum/nmSubWxs/nmWxGeoRefDlg.cpp b/Src/nmNum/nmSubWxs/nmWxGeoRefDlg.cpp index 73d16114..f5fce06f 100644 --- a/Src/nmNum/nmSubWxs/nmWxGeoRefDlg.cpp +++ b/Src/nmNum/nmSubWxs/nmWxGeoRefDlg.cpp @@ -1,789 +1,517 @@ -#define _USE_MATH_DEFINES - #include "nmWxGeoRefDlg.h" + +#include "iUnitGroup.h" +#include "iUnitHelper.h" +#include "ZxBaseUtil.h" + +#include +#include +#include #include -#include -#include -#include #include +#include +#include +#include +#include +#include #include -#include "nmDataAnalyzeManager.h" -#include "nmDataAxis.h" - -nmWxGeoRefDlg::nmWxGeoRefDlg(QWidget* parent) - : iDlgBase(parent), m_bUpdating(false) +namespace { - this->setWindowTitle(tr("Geo referencing")); - initUI(); - this->resize(370, 320); - this->setFixedSize(this->size()); - - m_geoRefData = nmDataAnalyzeManager::getCurrentInstance()->getGeoRefDataCopy(); - - loadDataToUI(); +// WGS84 投影计算只处理度和米,不访问控件、工程对象或显示单位。 +const double s_dPi = 3.14159265358979323846; +const double s_dSemiMajor = 6378137.0; // WGS84 长半轴,单位 m。 +const double s_dFlattening = 1.0 / 298.257223563; +const double s_dEccentricity2 = 2.0 * s_dFlattening - s_dFlattening * s_dFlattening; +const double s_dScale = 0.9996; // UTM 中央经线比例因子。 +const char s_szBands[] = "CDEFGHJKLMNPQRSTUVWX"; + +// 检查度制经纬度是否有限且位于 WGS84/UTM 支持范围内。 +bool isValidGeographic(double dLatitude, double dLongitude) +{ + // UTM 仅覆盖南纬 80 度至北纬 84 度;极区需要另一套投影,不能沿用 UTM 公式。 + return qIsFinite(dLatitude) && qIsFinite(dLongitude) + && dLatitude >= -80.0 && dLatitude <= 84.0 + && dLongitude >= -180.0 && dLongitude <= 180.0; } -void nmWxGeoRefDlg::initUI() +// 检查米制投影坐标的基本范围;画布显示范围不参与参考点合法性判断。 +bool isValidProjected(double dEasting, double dNorthing) { - // Location group - QGroupBox* locationGroup = new QGroupBox(tr("Location")); - - // form 布局 - QFormLayout* locForm = new QFormLayout; - locForm->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter); - locForm->setVerticalSpacing(12); - locForm->setHorizontalSpacing(20); - locForm->setContentsMargins(8, 8, 8, 8); - - m_pLongitudeEdit = new QLineEdit; m_pLongitudeEdit->setFixedWidth(180); - m_pLatitudeEdit = new QLineEdit; m_pLatitudeEdit ->setFixedWidth(180); - locForm->addRow(tr("Longitude:"), m_pLongitudeEdit); - locForm->addRow(tr("Latitude:"), m_pLatitudeEdit); - - // 包一层 vbox,用 stretch 让 form 居中 - QVBoxLayout* locWrapper = new QVBoxLayout; - locWrapper->addStretch(1); - locWrapper->addLayout(locForm); - locWrapper->addStretch(1); - locWrapper->setContentsMargins(0, 0, 0, 0); // 根据需要调整 groupbox 内边距 - locationGroup->setLayout(locWrapper); - - // Zone group - QGroupBox* zoneGroup = new QGroupBox(tr("Zone")); - - QFormLayout* zoneForm = new QFormLayout; - zoneForm->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter); - zoneForm->setVerticalSpacing(12); - zoneForm->setHorizontalSpacing(20); - zoneForm->setContentsMargins(8, 8, 8, 8); - - m_pUtmZoneCombo = new QComboBox; m_pUtmZoneCombo->setFixedWidth(180); - m_pEastingEdit = new QLineEdit; m_pEastingEdit ->setFixedWidth(180); - m_pNorthingEdit = new QLineEdit; m_pNorthingEdit->setFixedWidth(180); - zoneForm->addRow(tr("UTM zone:"), m_pUtmZoneCombo); - zoneForm->addRow(tr("Easting:"), m_pEastingEdit); - zoneForm->addRow(tr("Northing:"), m_pNorthingEdit); - - // 初始化 UTM Zone 下拉框(格式:01C~60X) - 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) { - // 格式化为两位数 Zone + 纬度带(例如 "01C", "02C") - QString zoneStr = QString("%1%2").arg(zone, 2, 10, QLatin1Char('0')).arg(band); - m_pUtmZoneCombo->addItem(zoneStr); - } - } - - m_pUtmZoneCombo->setCurrentIndex(-1); - - QVBoxLayout* zoneWrapper = new QVBoxLayout; - zoneWrapper->addStretch(1); - zoneWrapper->addLayout(zoneForm); - zoneWrapper->addStretch(1); - zoneWrapper->setContentsMargins(0, 0, 0, 0); - zoneGroup->setLayout(zoneWrapper); - - // Buttons - m_pOkButton = new QPushButton(tr("OK")); - m_pCancelButton = new QPushButton(tr("Cancel")); - m_pOkButton->setDefault(true); - connect(m_pOkButton, SIGNAL(clicked()), this, SLOT(okAccept())); - connect(m_pCancelButton, SIGNAL(clicked()), this, SLOT(cancelReject())); - - QHBoxLayout* btnLay = new QHBoxLayout; - btnLay->addStretch(); - btnLay->addWidget(m_pOkButton); - btnLay->addWidget(m_pCancelButton); - - // 主布局 - QVBoxLayout* mainLay = new QVBoxLayout; - mainLay->setContentsMargins(10, 10, 10, 10); - mainLay->setSpacing(15); - mainLay->addWidget(locationGroup); - mainLay->addWidget(zoneGroup); - mainLay->addLayout(btnLay); - setLayout(mainLay); - - // 初始都设成不可编辑 - m_pLongitudeEdit->setEnabled(false); - m_pLatitudeEdit ->setEnabled(false); - m_pUtmZoneCombo ->setEnabled(true); - m_pEastingEdit ->setEnabled(false); - m_pNorthingEdit ->setEnabled(false); - - connect(m_pUtmZoneCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onUtmZoneChanged(int))); - connect(m_pLongitudeEdit, SIGNAL(editingFinished()), this, SLOT(onLongitudeChanged())); - connect(m_pLatitudeEdit, SIGNAL(editingFinished()), this, SLOT(onLatitudeChanged())); - connect(m_pEastingEdit, SIGNAL(editingFinished()), this, SLOT(onEastingChanged())); - connect(m_pNorthingEdit, SIGNAL(editingFinished()), this, SLOT(onNorthingChanged())); + // 检查参考点本身,不随画布缩放范围改变允许输入的坐标范围。 + return qIsFinite(dEasting) && qIsFinite(dNorthing) + && dEasting >= 0.0 && dEasting <= 1000000.0 + && dNorthing >= 0.0 && dNorthing <= 10000000.0; } -// 根据经纬度和UTM zone计算Northing坐标的辅助函数 -double nmWxGeoRefDlg::calculateNorthing(double latitude, double longitude, int zone) +// 解析两位经度带号和一位纬度带字母;失败时输出参数不可用于转换。 +bool parseZone(const QString& sZone, int& nZone, char& cBand) { - // UTM投影的简化计算公式 - // 这里使用基本的UTM投影算法 - - const double a = 6378137.0; // WGS84长半轴 - const double e2 = 0.00669437999014; // WGS84第一偏心率的平方 - const double k0 = 0.9996; // UTM比例因子 - - // 转换为弧度 - double lat_rad = latitude * M_PI / 180.0; - double lon_rad = longitude * M_PI / 180.0; - - // 中央经线 - double lon0_rad = (-183.0 + zone * 6.0) * M_PI / 180.0; - double dlon = lon_rad - lon0_rad; - - // UTM投影计算 - double N = a / sqrt(1 - e2 * sin(lat_rad) * sin(lat_rad)); - double T = tan(lat_rad) * tan(lat_rad); - double C = e2 * cos(lat_rad) * cos(lat_rad) / (1 - e2); - double A = cos(lat_rad) * dlon; - - double M = a * ((1 - e2 / 4 - 3 * e2 * e2 / 64 - 5 * e2 * e2 * e2 / 256) * lat_rad - - (3 * e2 / 8 + 3 * e2 * e2 / 32 + 45 * e2 * e2 * e2 / 1024) * sin(2 * lat_rad) - + (15 * e2 * e2 / 256 + 45 * e2 * e2 * e2 / 1024) * sin(4 * lat_rad) - - (35 * e2 * e2 * e2 / 3072) * sin(6 * lat_rad)); - - double northing = k0 * (M + N * tan(lat_rad) * (A * A / 2 + (5 - T + 9 * C + 4 * C * C) * A * A * A * A / 24 - + (61 - 58 * T + T * T + 600 * C - 330 * e2) * A * A * A * A * A * A / 720)); - - // 南半球需要加上假北值 - if(latitude < 0) { - northing += 10000000.0; - } - - return northing; + bool bNumber = false; + nZone = sZone.left(2).toInt(&bNumber); + if (sZone.size() != 3 || !bNumber || nZone < 1 || nZone > 60) return false; + cBand = sZone.at(2).toLatin1(); + if (!QString::fromLatin1(s_szBands).contains(QChar(cBand))) return false; + // Svalbard 的 X 纬度带只有加宽后的奇数分带,32X/34X/36X 不存在。 + return cBand != 'X' || (nZone != 32 && nZone != 34 && nZone != 36); } -// 经度编辑完成时的处理 -void nmWxGeoRefDlg::onLongitudeChanged() +// 根据合法的度制纬度确定 C~X 纬度带,跳过 I、O。 +char latitudeBand(double dLatitude) { - if(m_bUpdating) return; // 防止递归更新 - - QString lonText = m_pLongitudeEdit->text(); - QString latText = m_pLatitudeEdit->text(); - - // 检查经度值是否真的发生了变化 - if(lonText == m_lastLongitude) { - return; // 值没有变化,不进行转换 - } - - if(!lonText.isEmpty() && !latText.isEmpty()) { - updateFromLatLon(); - } + // X 带覆盖 72~84 度,最后一个索引需截断,避免 84 度越界。 + const int nBand = qBound(0, static_cast(floor((dLatitude + 80.0) / 8.0)), 19); + return s_szBands[nBand]; } -// 纬度编辑完成时的处理 -void nmWxGeoRefDlg::onLatitudeChanged() +// 生成与旧工程一致的三字符分带标识,如 01C、50S。 +QString zoneText(int nZone, char cBand) { - if(m_bUpdating) return; // 防止递归更新 - - QString lonText = m_pLongitudeEdit->text(); - QString latText = m_pLatitudeEdit->text(); - - // 检查纬度值是否真的发生了变化 - if(latText == m_lastLatitude) { - return; // 值没有变化,不进行转换 - } - - if(!lonText.isEmpty() && !latText.isEmpty()) { - updateFromLatLon(); - } + return QString("%1%2").arg(nZone, 2, 10, QLatin1Char('0')).arg(QChar(cBand)); } -// 东向坐标编辑完成时的处理 -void nmWxGeoRefDlg::onEastingChanged() +// 按合法的度制经纬度选择自然 UTM 经度带,包含区域特例及日期变更线边界。 +int longitudeZone(double dLatitude, double dLongitude) { - if(m_bUpdating) return; // 防止递归更新 - - QString eastingText = m_pEastingEdit->text(); - QString northingText = m_pNorthingEdit->text(); - QString zoneText = m_pUtmZoneCombo->currentText(); - - // 检查东向坐标值是否真的发生了变化 - if(eastingText == m_lastEasting) { - return; // 值没有变化,不进行转换 - } - - if(!eastingText.isEmpty() && !northingText.isEmpty() && !zoneText.isEmpty()) { - updateFromUTM(); + // +180 度属于第 60 带,不能产生不存在的第 61 带。 + int nZone = qBound(1, static_cast(floor((dLongitude + 180.0) / 6.0)) + 1, 60); + // 按 UTM 约定处理 Norway 和 Svalbard 的特殊带宽。 + if (dLatitude >= 56.0 && dLatitude < 64.0 && dLongitude >= 3.0 && dLongitude < 12.0) + nZone = 32; + if (dLatitude >= 72.0 && dLongitude >= 0.0 && dLongitude < 42.0) { + if (dLongitude < 9.0) nZone = 31; + else if (dLongitude < 21.0) nZone = 33; + else if (dLongitude < 33.0) nZone = 35; + else nZone = 37; } + return nZone; } -// 北向坐标编辑完成时的处理 -void nmWxGeoRefDlg::onNorthingChanged() +// WGS84 正投影:输入纬度/经度(度),成功后输出自然分带和东向/北向坐标(米)。 +bool geographicToUtm(double dLatitude, double dLongitude, QString& sZone, + double& dEasting, double& dNorthing) { - if(m_bUpdating) return; // 防止递归更新 - - QString eastingText = m_pEastingEdit->text(); - QString northingText = m_pNorthingEdit->text(); - QString zoneText = m_pUtmZoneCombo->currentText(); - - // 检查北向坐标值是否真的发生了变化 - if(northingText == m_lastNorthing) { - return; // 值没有变化,不进行转换 - } - - if(!eastingText.isEmpty() && !northingText.isEmpty() && !zoneText.isEmpty()) { - updateFromUTM(); - } + if (!isValidGeographic(dLatitude, dLongitude)) return false; + const int nZone = longitudeZone(dLatitude, dLongitude); + const double dLat = dLatitude * s_dPi / 180.0; + const double dDeltaLon = (dLongitude - (-183.0 + nZone * 6.0)) * s_dPi / 180.0; + const double dE2 = s_dEccentricity2; + const double dEp2 = dE2 / (1.0 - dE2); // 第二偏心率平方,用于横轴墨卡托级数系数。 + const double dN = s_dSemiMajor / sqrt(1.0 - dE2 * sin(dLat) * sin(dLat)); + const double dT = tan(dLat) * tan(dLat); + const double dC = dEp2 * cos(dLat) * cos(dLat); + const double dA = cos(dLat) * dDeltaLon; + const double dA2 = dA * dA; + const double dM = s_dSemiMajor * ( + (1.0 - dE2 / 4.0 - 3.0 * dE2 * dE2 / 64.0 - 5.0 * dE2 * dE2 * dE2 / 256.0) * dLat + - (3.0 * dE2 / 8.0 + 3.0 * dE2 * dE2 / 32.0 + 45.0 * dE2 * dE2 * dE2 / 1024.0) * sin(2.0 * dLat) + + (15.0 * dE2 * dE2 / 256.0 + 45.0 * dE2 * dE2 * dE2 / 1024.0) * sin(4.0 * dLat) + - 35.0 * dE2 * dE2 * dE2 / 3072.0 * sin(6.0 * dLat)); + dEasting = 500000.0 + s_dScale * dN * (dA + (1.0 - dT + dC) * dA * dA2 / 6.0 + + (5.0 - 18.0 * dT + dT * dT + 72.0 * dC - 58.0 * dEp2) * dA * dA2 * dA2 / 120.0); + dNorthing = s_dScale * (dM + dN * tan(dLat) * (dA2 / 2.0 + + (5.0 - dT + 9.0 * dC + 4.0 * dC * dC) * dA2 * dA2 / 24.0 + + (61.0 - 58.0 * dT + dT * dT + 600.0 * dC - 330.0 * dEp2) * dA2 * dA2 * dA2 / 720.0)); + if (dLatitude < 0.0) dNorthing += 10000000.0; // 南半球假北值。 + sZone = zoneText(nZone, latitudeBand(dLatitude)); + return isValidProjected(dEasting, dNorthing); } -// 更新记录的值 -void nmWxGeoRefDlg::updateFromLatLon() +// WGS84 反投影:输入已校验的分带及米制坐标,成功后输出纬度/经度(度)。 +bool utmToGeographic(int nZone, char cBand, double dEasting, double dNorthing, + double& dLatitude, double& dLongitude) { - m_bUpdating = true; // 设置更新标志 - - bool ok1, ok2; - double latitude = m_pLatitudeEdit->text().toDouble(&ok1); - double longitude = m_pLongitudeEdit->text().toDouble(&ok2); - - if(!ok1 || !ok2) { - QMessageBox::warning(this, tr("Input Error"), tr("Please enter valid numbers")); - // 恢复之前的有效值 - m_pLongitudeEdit->setText(m_lastLongitude); - m_pLatitudeEdit->setText(m_lastLatitude); - m_bUpdating = false; - return; - } - - // 验证经纬度范围 - if(!isValidLatitude(latitude)) { - QMessageBox::warning(this, tr("Input Error"), tr("Latitude must be between - 90o and + 90o")); - // 恢复之前的有效值 - m_pLatitudeEdit->setText(m_lastLatitude); - m_bUpdating = false; - return; - } - - if(!isValidLongitude(longitude)) { - QMessageBox::warning(this, tr("Input Error"), tr("Longitude must be between - 180o and + 180o")); - // 恢复之前的有效值 - m_pLongitudeEdit->setText(m_lastLongitude); - m_bUpdating = false; - return; - } - - int zone; - char band; - double easting, northing; - - // 执行坐标转换 - latLonToUTM(latitude, longitude, zone, band, easting, northing); - - // 更新UTM Zone下拉框 - QString zoneStr = QString("%1%2").arg(zone, 2, 10, QLatin1Char('0')).arg(band); - int index = m_pUtmZoneCombo->findText(zoneStr); - - if(index >= 0) { - m_pUtmZoneCombo->setCurrentIndex(index); - } - - // 更新东向和北向坐标 - QString eastingStr = QString::number(easting, 'f', 2); - QString northingStr = QString::number(northing, 'f', 2); - - m_pEastingEdit->setText(eastingStr); - m_pNorthingEdit->setText(northingStr); - - // 转换成功后,更新所有记录的值 - m_lastLongitude = m_pLongitudeEdit->text(); // 使用当前输入框的值 - m_lastLatitude = m_pLatitudeEdit->text(); - m_lastEasting = eastingStr; - m_lastNorthing = northingStr; - - m_bUpdating = false; // 清除更新标志 + if (!isValidProjected(dEasting, dNorthing)) return false; + const double dE2 = s_dEccentricity2; + const double dEp2 = dE2 / (1.0 - dE2); + const double dE1 = (1.0 - sqrt(1.0 - dE2)) / (1.0 + sqrt(1.0 - dE2)); + const double dY = dNorthing - (cBand < 'N' ? 10000000.0 : 0.0); + const double dMu = dY / (s_dScale * s_dSemiMajor + * (1.0 - dE2 / 4.0 - 3.0 * dE2 * dE2 / 64.0 - 5.0 * dE2 * dE2 * dE2 / 256.0)); + const double dPhi = dMu + (3.0 * dE1 / 2.0 - 27.0 * pow(dE1, 3.0) / 32.0) * sin(2.0 * dMu) + + (21.0 * dE1 * dE1 / 16.0 - 55.0 * pow(dE1, 4.0) / 32.0) * sin(4.0 * dMu) + + 151.0 * pow(dE1, 3.0) / 96.0 * sin(6.0 * dMu) + + 1097.0 * pow(dE1, 4.0) / 512.0 * sin(8.0 * dMu); + if (!qIsFinite(dPhi) || qAbs(cos(dPhi)) < 1e-6) return false; + const double dC = dEp2 * cos(dPhi) * cos(dPhi); + const double dT = tan(dPhi) * tan(dPhi); + const double dN = s_dSemiMajor / sqrt(1.0 - dE2 * sin(dPhi) * sin(dPhi)); + const double dR = s_dSemiMajor * (1.0 - dE2) / pow(1.0 - dE2 * sin(dPhi) * sin(dPhi), 1.5); + const double dD = (dEasting - 500000.0) / (dN * s_dScale); + const double dD2 = dD * dD; + dLatitude = (dPhi - dN * tan(dPhi) / dR * (dD2 / 2.0 + - (5.0 + 3.0 * dT + 10.0 * dC - 4.0 * dC * dC - 9.0 * dEp2) * dD2 * dD2 / 24.0 + + (61.0 + 90.0 * dT + 298.0 * dC + 45.0 * dT * dT - 252.0 * dEp2 - 3.0 * dC * dC) + * dD2 * dD2 * dD2 / 720.0)) * 180.0 / s_dPi; + dLongitude = -183.0 + nZone * 6.0 + (dD - (1.0 + 2.0 * dT + dC) * dD * dD2 / 6.0 + + (5.0 - 2.0 * dC + 28.0 * dT - 3.0 * dC * dC + 8.0 * dEp2 + 24.0 * dT * dT) + * dD * dD2 * dD2 / 120.0) / cos(dPhi) * 180.0 / s_dPi; + if (!qIsFinite(dLatitude) || !qIsFinite(dLongitude)) return false; + // 仅容忍投影级数在覆盖边界的微小数值误差,不把极区坐标截成合法纬度。 + if (dLatitude < -80.0000001 || dLatitude > 84.0000001) return false; + dLatitude = qBound(-80.0, dLatitude, 84.0); + if ((cBand < 'N' && dLatitude >= 0.0) || (cBand >= 'N' && dLatitude < 0.0)) return false; + if (dLongitude > 180.0) dLongitude -= 360.0; + if (dLongitude < -180.0) dLongitude += 360.0; + return isValidGeographic(dLatitude, dLongitude); } - -// 更新记录的值 -void nmWxGeoRefDlg::updateFromUTM() +// 为已校验的分带求覆盖区域中心(度),供用户主动选带时初始化参考点。 +void zoneCenter(int nZone, char cBand, double& dLatitude, double& dLongitude) { - m_bUpdating = true; // 设置更新标志 - - bool ok1, ok2; - double easting = m_pEastingEdit->text().toDouble(&ok1); - double northing = m_pNorthingEdit->text().toDouble(&ok2); - - QString zoneText = m_pUtmZoneCombo->currentText(); - - if(!ok1 || !ok2) { - QMessageBox::warning(this, tr("Input Error"), tr("Please enter valid numbers")); - restoreLastValidValues(); - return; - } - - // 验证UTM坐标范围 - if(!isValidEasting(easting)) { - QMessageBox::warning(this, tr("Input Error"), tr("Easting must be between 0 and 1,000,000 meters")); - restoreLastValidValues(); - return; - } - - if(!isValidNorthing(northing)) { - QMessageBox::warning(this, tr("Input Error"), tr("Northing must be between 0 and 10,000,000 meters")); - restoreLastValidValues(); - return; - } - - if(zoneText.isEmpty()) { - QMessageBox::warning(this, tr("Input Error"), tr("Please select a valid UTM Zone")); - m_bUpdating = false; - return; - } - - // 解析UTM Zone - int zone = zoneText.left(2).toInt(); - char band = zoneText.right(1).toLocal8Bit().at(0); - - double latitude, longitude; - - // 执行坐标转换,检查返回值 - bool conversionSuccess = utmToLatLon(zone, band, easting, northing, latitude, longitude); - - if (!conversionSuccess) { - // 根据具体失败原因给出不同的错误信息 - if (band == 'C' || band == 'X' || zone <= 2 || zone >= 59) { - QMessageBox::warning(this, tr("Conversion Error"), - tr("The UTM coordinates you entered correspond to an extreme polar region where coordinate conversion may be inaccurate.\n" - "Please try coordinates closer to the center of the UTM zone.")); - } else { - QMessageBox::warning(this, tr("Conversion Error"), - tr("UTM coordinate conversion failed.\n" - "The coordinates may be outside the valid range for this UTM zone.\n" - "Please check your input coordinates.")); - } - restoreLastValidValues(); - return; - } - - // 验证转换结果 - if(!isValidLatitude(latitude) || !isValidLongitude(longitude)) { - QMessageBox::warning(this, tr("Conversion Error"), tr("UTM coordinate conversion result is out of valid range, please check input")); - restoreLastValidValues(); - return; - } - - // 更新经纬度 - QString longitudeStr = QString::number(longitude, 'f', 6); - QString latitudeStr = QString::number(latitude, 'f', 6); - - m_pLongitudeEdit->setText(longitudeStr); - m_pLatitudeEdit->setText(latitudeStr); - - // 转换成功后,更新所有记录的值 - m_lastLongitude = longitudeStr; - m_lastLatitude = latitudeStr; - m_lastEasting = m_pEastingEdit->text(); // 使用当前输入框的值 - m_lastNorthing = m_pNorthingEdit->text(); - - m_bUpdating = false; // 清除更新标志 + const int nBand = QString::fromLatin1(s_szBands).indexOf(QChar(cBand)); + dLatitude = cBand == 'X' ? 78.0 : -76.0 + nBand * 8.0; + dLongitude = -183.0 + nZone * 6.0; + // 特殊加宽/缩窄带使用实际覆盖区域中心,确保初始化后仍属于用户选中的带。 + if (cBand == 'V' && nZone == 31) dLongitude = 1.5; + if (cBand == 'V' && nZone == 32) dLongitude = 7.5; + if (cBand == 'X' && nZone == 31) dLongitude = 4.5; + if (cBand == 'X' && nZone == 37) dLongitude = 37.5; } -// 恢复上次有效值 -void nmWxGeoRefDlg::restoreLastValidValues() +// 创建与模型位置、测量工具一致的三列坐标网格,布局所有权交给分组控件。 +QGridLayout* createCoordinateGrid(QGroupBox* pGroup) { - m_pEastingEdit->setText(m_lastEasting); - m_pNorthingEdit->setText(m_lastNorthing); - m_bUpdating = false; + 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(1, 8); + pGrid->setColumnStretch(2, 5); + return pGrid; +} } -// 更新记录的值 -void nmWxGeoRefDlg::onUtmZoneChanged(int index) +nmWxGeoRefDlg::nmWxGeoRefDlg(const nmDataGeoRef& oData, QWidget* pParent) + : iDlgBase(pParent), m_oGeoRefData(oData), + m_pLengthUnitGroup(iUnitHelper::getUnitGroupByUnit("m")), + m_eLastInput(NoInput), m_bHasCoordinates(false), m_bInputValid(false), + m_bRefreshing(false), m_bChanged(false), m_pUtmZoneComboBox(nullptr), m_pOkButton(nullptr) { - // 添加这行检查!如果正在更新过程中,直接返回 - if(m_bUpdating) return; - - // 设置更新标志,防止触发其他转换函数 - m_bUpdating = true; - - bool enabled = (index >= 0); // 选择了有效的 UTM Zone 时启用 - m_pLongitudeEdit->setEnabled(enabled); - m_pLatitudeEdit ->setEnabled(enabled); - m_pEastingEdit ->setEnabled(enabled); - m_pNorthingEdit ->setEnabled(enabled); - - if(enabled) { - // 获取选中的 UTM Zone - QString selectedZone = m_pUtmZoneCombo->itemText(index); - - // 从选中的 UTM Zone 提取 Zone 和 Band - int zone = selectedZone.left(2).toInt(); - QString band = selectedZone.right(1); - - // 计算经度(Longitude) - double longitude = -177.0 + (zone - 1) * 6.0; - QString longitudeStr = QString::number(longitude, 'f', 6); - m_pLongitudeEdit->setText(longitudeStr); - - // 计算纬度(Latitude) - QStringList bands; - bands << "C" << "D" << "E" << "F" << "G" << "H" << "J" << "K" << "L" << "M" - << "N" << "P" << "Q" << "R" << "S" << "T" << "U" << "V" << "W" << "X"; - - int bandIndex = bands.indexOf(band); - double latitude = -76.0; - - if(bandIndex >= 0) { - if(bandIndex < bands.indexOf("X")) { - // X之前的所有band,每个递增8度 - latitude += bandIndex * 8.0; - } else { - // X band:W到X跨度为10度 - latitude += (bandIndex - 1) * 8.0 + 10.0; - } - } - - QString latitudeStr = QString::number(latitude, 'f', 6); - m_pLatitudeEdit->setText(latitudeStr); - - // 设置东向坐标(Easting) - double easting = 50000.00; - QString eastingStr = QString::number(easting, 'f', 2); - m_pEastingEdit->setText(eastingStr); - - // 设置北向坐标(Northing) - double northing = calculateNorthing(latitude, longitude, zone); - QString northingStr = QString::number(northing, 'f', 2); - m_pNorthingEdit->setText(northingStr); - - // 更新记录的所有值 - m_lastLongitude = longitudeStr; - m_lastLatitude = latitudeStr; - m_lastEasting = eastingStr; - m_lastNorthing = northingStr; + for (int nField = 0; nField < FieldCount; ++nField) { + m_dBaseValues[nField] = 0.0; + m_pCoordinateEdits[nField] = nullptr; + m_pLengthUnits[nField] = nullptr; + m_sDisplayUnits[nField] = nField < Easting ? "o" : "m"; } - - // 清除更新标志 - m_bUpdating = false; + setWindowTitle(tr("Geo referencing")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + initUI(); + loadDataToUI(); + adjustSize(); // 初始尺寸来自字体、主题与布局,允许后续拉伸。 } -// 经纬度转UTM坐标 -void nmWxGeoRefDlg::latLonToUTM(double lat, double lon, int& zone, char& band, double& easting, double& northing) +void nmWxGeoRefDlg::initUI() { - // 确定UTM Zone - zone = getUTMZoneFromLongitude(lon); - band = getUTMBandFromLatitude(lat); - - // WGS84椭球体参数 - const double a = 6378137.0; // 长半轴 - const double e2 = 0.00669437999014; // 第一偏心率的平方 - const double k0 = 0.9996; // 比例因子 - - // 转换为弧度 - double lat_rad = lat * M_PI / 180.0; - double lon_rad = lon * M_PI / 180.0; - - // 中央经线 - double lon0 = -183.0 + zone * 6.0; - double lon0_rad = lon0 * M_PI / 180.0; - double dlon = lon_rad - lon0_rad; - - // UTM投影计算 - double N = a / sqrt(1 - e2 * sin(lat_rad) * sin(lat_rad)); - double T = tan(lat_rad) * tan(lat_rad); - double C = e2 * cos(lat_rad) * cos(lat_rad) / (1 - e2); - double A = cos(lat_rad) * dlon; - - double M = a * ((1 - e2 / 4 - 3 * e2 * e2 / 64 - 5 * e2 * e2 * e2 / 256) * lat_rad - - (3 * e2 / 8 + 3 * e2 * e2 / 32 + 45 * e2 * e2 * e2 / 1024) * sin(2 * lat_rad) - + (15 * e2 * e2 / 256 + 45 * e2 * e2 * e2 / 1024) * sin(4 * lat_rad) - - (35 * e2 * e2 * e2 / 3072) * sin(6 * lat_rad)); - - // 计算东向坐标 - easting = k0 * N * (A + (1 - T + C) * A * A * A / 6 - + (5 - 18 * T + T * T + 72 * C - 58 * e2) * A * A * A * A * A / 120) + 500000.0; - - // 计算北向坐标 - northing = k0 * (M + N * tan(lat_rad) * (A * A / 2 + (5 - T + 9 * C + 4 * C * C) * A * A * A * A / 24 - + (61 - 58 * T + T * T + 600 * C - 330 * e2) * A * A * A * A * A * A / 720)); - - // 南半球需要加上假北值 - if(lat < 0) { - northing += 10000000.0; + QVBoxLayout* pMain = new QVBoxLayout(this); + pMain->setContentsMargins(12, 10, 12, 0); + pMain->setSpacing(10); + + QGroupBox* pLocationGroup = new QGroupBox(tr("Location"), this); + QGridLayout* pLocationGrid = createCoordinateGrid(pLocationGroup); + const QString sDegree = QString(" (%1)").arg(QChar(0x00b0)); + createCoordinateField(pLocationGrid, 0, Longitude, tr("Longitude") + sDegree, "geoLongitude"); + createCoordinateField(pLocationGrid, 1, Latitude, tr("Latitude") + sDegree, "geoLatitude"); + pMain->addWidget(pLocationGroup); + + QGroupBox* pProjectionGroup = new QGroupBox(tr("Zone"), this); + QGridLayout* pProjectionGrid = createCoordinateGrid(pProjectionGroup); + QLabel* pZoneLabel = new QLabel(tr("UTM zone"), pProjectionGroup); + pZoneLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + pProjectionGrid->addWidget(pZoneLabel, 0, 0); + m_pUtmZoneComboBox = new QComboBox(pProjectionGroup); + m_pUtmZoneComboBox->setObjectName("geoUtmZone"); + m_pUtmZoneComboBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + m_pUtmZoneComboBox->setSizeAdjustPolicy(QComboBox::AdjustToContents); + // 分带属于坐标标识,不是长度单位;选项沿用数据模型,过滤不存在的特殊带。 + const QStringList listZones = m_oGeoRefData.getUtmZone().getListValueSelections(); + foreach (const QString& sZone, listZones) { + int nZone = 0; + char cBand = 0; + if (parseZone(sZone, nZone, cBand)) m_pUtmZoneComboBox->addItem(sZone); } + m_pUtmZoneComboBox->setCurrentIndex(-1); + pProjectionGrid->addWidget(m_pUtmZoneComboBox, 0, 1, 1, 2); + createCoordinateField(pProjectionGrid, 1, Easting, tr("Easting"), "geoEasting"); + createCoordinateField(pProjectionGrid, 2, Northing, tr("Northing"), "geoNorthing"); + pMain->addWidget(pProjectionGroup); + + // 两个分组按实际标签宽度对齐;数值与单位按 8:5 横向分配剩余宽度。 + int nLabelWidth = 0; + const QList listLabels = findChildren(); + foreach (QLabel* pLabel, listLabels) nLabelWidth = qMax(nLabelWidth, pLabel->sizeHint().width()); + pLocationGrid->setColumnMinimumWidth(0, nLabelWidth); + pProjectionGrid->setColumnMinimumWidth(0, nLabelWidth); + pMain->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::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, pFooter); + m_pOkButton = pButtons->button(QDialogButtonBox::Ok); + m_pOkButton->setText(tr("OK")); + m_pOkButton->setIcon(zxLoadIcon("OK")); + m_pOkButton->setDefault(true); + QPushButton* pCancel = pButtons->button(QDialogButtonBox::Cancel); + pCancel->setText(tr("Cancel")); + pCancel->setIcon(zxLoadIcon("Cancel")); + pCancel->setAutoDefault(false); + pFooterLayout->addWidget(pButtons); + pMain->addWidget(pFooter); + connect(pButtons, SIGNAL(accepted()), this, SLOT(accept())); + connect(pButtons, SIGNAL(rejected()), this, SLOT(reject())); + connect(m_pUtmZoneComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(onUtmZoneChanged(int))); } -// UTM坐标转经纬度 -bool nmWxGeoRefDlg::utmToLatLon(int zone, char band, double easting, double northing, double& lat, double& lon) +void nmWxGeoRefDlg::createCoordinateField(QGridLayout* pGrid, int nRow, CoordinateField eField, + const QString& sLabel, const QString& sObjectName) { - // WGS84椭球体参数 - const double a = 6378137.0; // 长半轴 (米) - const double f = 1.0 / 298.257223563; // 扁率 - const double e2 = 2 * f - f * f; // 第一偏心率的平方 - const double k0 = 0.9996; // UTM比例因子 - const double e1 = (1 - sqrt(1 - e2)) / (1 + sqrt(1 - e2)); - - // 中央经线经度 - double lon0 = -183.0 + zone * 6.0; - - // 调整北向坐标(南半球处理) - double y = northing; - if(band < 'N') { // 南半球 - y -= 10000000.0; - } - - // 调整东向坐标 - double x = easting - 500000.0; - - // 计算足点纬度 - double M = y / k0; - double mu = M / (a * (1 - e2 / 4 - 3 * e2 * e2 / 64 - 5 * e2 * e2 * e2 / 256)); - - // 使用级数展开计算足点纬度 - double phi1 = mu + (3 * e1 / 2 - 27 * e1 * e1 * e1 / 32) * sin(2 * mu) - + (21 * e1 * e1 / 16 - 55 * e1 * e1 * e1 * e1 / 32) * sin(4 * mu) - + (151 * e1 * e1 * e1 / 96) * sin(6 * mu) - + (1097 * e1 * e1 * e1 * e1 / 512) * sin(8 * mu); - - // cos(phi1)是否过小 - if (fabs(cos(phi1)) < 1e-6) { - // 设置默认值,让调用方知道转换失败 - lat = 0.0; - lon = 0.0; - return false; // 返回false表示转换失败 - } - - // 计算各种参数 - double C1 = e2 * cos(phi1) * cos(phi1) / (1 - e2); - double T1 = tan(phi1) * tan(phi1); - double N1 = a / sqrt(1 - e2 * sin(phi1) * sin(phi1)); - double R1 = a * (1 - e2) / pow(1 - e2 * sin(phi1) * sin(phi1), 1.5); - double D = x / (N1 * k0); - - // 计算纬度(弧度) - double lat_rad = phi1 - (N1 * tan(phi1) / R1) * - (D * D / 2 - (5 + 3 * T1 + 10 * C1 - 4 * C1 * C1 - 9 * e2) * D * D * D * D / 24 - + (61 + 90 * T1 + 298 * C1 + 45 * T1 * T1 - 252 * e2 - 3 * C1 * C1) * D * D * D * D * D * D / 720); - - // 计算经度(弧度) - double lon_rad = (D - (1 + 2 * T1 + C1) * D * D * D / 6 - + (5 - 2 * C1 + 28 * T1 - 3 * C1 * C1 + 8 * e2 + 24 * T1 * T1) * D * D * D * D * D / 120) / cos(phi1); - - // 转换为度并加上中央经线 - lat = lat_rad * 180.0 / M_PI; - lon = lon0 + lon_rad * 180.0 / M_PI; - - // 第二个检查:确保结果在合理范围内 - if (lat != lat || lon != lon || // 检查NaN - fabs(lat) > 90.0 || // 纬度必须在-90到90之间 - fabs(lon - lon0) > 180.0) { // 经度距离中央经线不应超过180度 - - // 设置默认值 - lat = 0.0; - lon = 0.0; - return false; // 返回false表示转换失败 - } - - // 确保经度在有效范围内 - while(lon > 180.0) lon -= 360.0; - while(lon < -180.0) lon += 360.0; - - return true; // 转换成功 + 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->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + m_pCoordinateEdits[eField] = pEdit; + pGrid->addWidget(pEdit, nRow, 1, 1, eField < Easting ? 2 : 1); + connect(pEdit, SIGNAL(textEdited(QString)), this, SLOT(onCoordinateEdited(QString))); + if (eField < Easting) return; + + QComboBox* pUnit = new QComboBox(pParent); + pUnit->setObjectName(sObjectName + "Unit"); + pUnit->setSizeAdjustPolicy(QComboBox::AdjustToContents); + pUnit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + if (m_pLengthUnitGroup) pUnit->addItems(m_pLengthUnitGroup->getAllUnitNames()); + pUnit->setCurrentIndex(pUnit->findText("m")); + m_pLengthUnits[eField] = pUnit; + pGrid->addWidget(pUnit, nRow, 2); + connect(pUnit, SIGNAL(currentIndexChanged(int)), this, SLOT(onLengthUnitChanged(int))); } -// 根据经度确定UTM Zone -int nmWxGeoRefDlg::getUTMZoneFromLongitude(double longitude) +bool nmWxGeoRefDlg::unitsAvailable() const { - return (int)floor((longitude + 180.0) / 6.0) + 1; + return m_pLengthUnitGroup && m_pLengthUnits[Easting]->findText("m") >= 0 + && m_pLengthUnits[Northing]->findText("m") >= 0; } -// 根据纬度确定UTM Band -char nmWxGeoRefDlg::getUTMBandFromLatitude(double latitude) +QString nmWxGeoRefDlg::displayValue(CoordinateField eField, double dValue, const QString& sUnit) const { - const char bands[] = "CDEFGHJKLMNPQRSTUVWX"; - int index = (int)floor((latitude + 80.0) / 8.0); - - if(index < 0) return 'C'; - - if(index >= 20) return 'X'; - - return bands[index]; + int nDigits = 6; // 经纬度保持六位小数;投影坐标使用框架目标单位的显示精度。 + if (eField >= Easting && (!m_pLengthUnitGroup + || !m_pLengthUnitGroup->convert("m", dValue, sUnit, dValue, nDigits))) return QString(); + if (!qIsFinite(dValue)) return QString(); + const QString sText = nDigits >= 0 ? QString::number(dValue, 'f', qMin(nDigits, 15)) + : QString::number(dValue, 'g', 12); + return dValue != 0.0 && sText.toDouble() == 0.0 ? QString::number(dValue, 'g', 12) : sText; } -bool nmWxGeoRefDlg::isValidLatitude(double lat) +void nmWxGeoRefDlg::loadDataToUI() { - return (lat >= -90.0 && lat <= 90.0); + m_bRefreshing = true; + const QVariant values[FieldCount] = { + m_oGeoRefData.getLongitude().getValue(), m_oGeoRefData.getLatitude().getValue(), + m_oGeoRefData.getEasting().getValue(), m_oGeoRefData.getNorthing().getValue() + }; + bool bComplete = true; // 空值不能被 toDouble 默认转零后当成合法参考点。 + for (int nField = 0; nField < FieldCount; ++nField) { + bool bNumber = false; + const double dValue = values[nField].toDouble(&bNumber); + bNumber = bNumber && qIsFinite(dValue); + bComplete = bComplete && bNumber; + m_dBaseValues[nField] = bNumber ? dValue : 0.0; + m_sShownTexts[nField] = bNumber + ? displayValue(static_cast(nField), dValue, m_sDisplayUnits[nField]) + : values[nField].toString(); + m_pCoordinateEdits[nField]->setText(m_sShownTexts[nField]); + } + m_sUtmZone = m_oGeoRefData.getUtmZone().getValue().toString().trimmed(); + m_pUtmZoneComboBox->setCurrentIndex(m_pUtmZoneComboBox->findText(m_sUtmZone)); + int nZone = 0; + char cBand = 0; + m_bHasCoordinates = bComplete && parseZone(m_sUtmZone, nZone, cBand) + && m_pUtmZoneComboBox->currentIndex() >= 0 + && isValidGeographic(m_dBaseValues[Latitude], m_dBaseValues[Longitude]) + && isValidProjected(m_dBaseValues[Easting], m_dBaseValues[Northing]); + m_bInputValid = m_bHasCoordinates; + // 加载时不重新投影或写回舍入值,避免仅打开旧工程就移动参考点。 + m_bRefreshing = false; + updateActions(); } -// 验证经度范围 -bool nmWxGeoRefDlg::isValidLongitude(double lon) +bool nmWxGeoRefDlg::readField(CoordinateField eField, double& dValue) const { - return (lon >= -180.0 && lon <= 180.0); + const QString sText = m_pCoordinateEdits[eField]->text(); + if (m_bHasCoordinates && sText == m_sShownTexts[eField]) { + dValue = m_dBaseValues[eField]; + return qIsFinite(dValue); + } + bool bNumber = false; + dValue = sText.trimmed().toDouble(&bNumber); + if (!bNumber || !qIsFinite(dValue)) return false; + if (eField < Easting) return true; + int nDigits = -1; // 读取原始换算结果,不按显示小数位舍入。 + return m_pLengthUnitGroup + && m_pLengthUnitGroup->convert(m_sDisplayUnits[eField], dValue, "m", dValue, nDigits) + && qIsFinite(dValue); } -// 验证东向坐标范围 -bool nmWxGeoRefDlg::isValidEasting(double easting) +bool nmWxGeoRefDlg::updateCoordinates(InputSource eSource) { - // 基本UTM范围检查 - if (easting < 0.0 || easting > 1000000.0) { - return false; - } - - // 获取坐标轴数据,考虑画布范围 - nmDataAxis* axisData = nmDataAnalyzeManager::getCurrentInstance()->getAxisData(); - if (axisData) { - double xMin = axisData->getXMin().getValue().toDouble(); - double xMax = axisData->getXMax().getValue().toDouble(); - - // 计算基准坐标的安全范围 - double safeEastingMin = 160000.0 - xMin; // UTM东向最小值 - 画布最小X - double safeEastingMax = 840000.0 - xMax; // UTM东向最大值 - 画布最大X - - // 确保范围有效 - if (safeEastingMin < 0.0) safeEastingMin = 0.0; - if (safeEastingMax > 1000000.0) safeEastingMax = 1000000.0; - if (safeEastingMin >= safeEastingMax) { - safeEastingMin = 0.0; - safeEastingMax = 1000000.0; - } - - return (easting >= safeEastingMin && easting <= safeEastingMax); - } - - return true; // 如果没有坐标轴数据,使用默认验证 + double dLongitude = 0.0, dLatitude = 0.0; // 待提交的地理坐标,单位为度。 + double dEasting = 0.0, dNorthing = 0.0; // 待提交的投影坐标,单位为米。 + QString sZone = m_pUtmZoneComboBox->currentText(); + bool bValid = unitsAvailable(); + if (eSource == GeographicInput) { + bValid = bValid && readField(Longitude, dLongitude) && readField(Latitude, dLatitude) + && geographicToUtm(dLatitude, dLongitude, sZone, dEasting, dNorthing); + } else if (eSource == ProjectedInput) { + int nZone = 0; + char cBand = 0; + bValid = bValid && parseZone(sZone, nZone, cBand) + && readField(Easting, dEasting) && readField(Northing, dNorthing) + && utmToGeographic(nZone, cBand, dEasting, dNorthing, dLatitude, dLongitude); + if (bValid) { + // 字母表示纬度带,不仅是南北半球;北向坐标跨带后同步更新字母。 + sZone = zoneText(nZone, latitudeBand(dLatitude)); + bValid = parseZone(sZone, nZone, cBand); + } + } else { + return m_bInputValid; + } + bValid = bValid && m_pUtmZoneComboBox->findText(sZone) >= 0; + m_bInputValid = bValid; + if (bValid) applyCoordinates(dLongitude, dLatitude, dEasting, dNorthing, sZone, eSource); + updateActions(); + return bValid; } -// 验证北向坐标范围 -bool nmWxGeoRefDlg::isValidNorthing(double northing) +void nmWxGeoRefDlg::applyCoordinates(double dLongitude, double dLatitude, double dEasting, + double dNorthing, const QString& sZone, InputSource eSource) { - // 基本UTM范围检查 - if (northing < 0.0 || northing > 10000000.0) { - return false; - } - - // 获取坐标轴数据,考虑画布范围 - nmDataAxis* axisData = nmDataAnalyzeManager::getCurrentInstance()->getAxisData(); - if (axisData) { - double yMin = axisData->getYMin().getValue().toDouble(); - double yMax = axisData->getYMax().getValue().toDouble(); - - // 计算基准坐标的安全范围 - double safeNorthingMin = 0.0 - yMin; // UTM北向最小值 - 画布最小Y - double safeNorthingMax = 9000000.0 - yMax; // UTM北向最大值 - 画布最大Y - - // 确保范围有效 - if (safeNorthingMin < 0.0) safeNorthingMin = 0.0; - if (safeNorthingMax > 10000000.0) safeNorthingMax = 10000000.0; - if (safeNorthingMin >= safeNorthingMax) { - safeNorthingMin = 0.0; - safeNorthingMax = 10000000.0; - } - - return (northing >= safeNorthingMin && northing <= safeNorthingMax); - } - - return true; // 如果没有坐标轴数据,使用默认验证 + m_bRefreshing = true; + m_dBaseValues[Longitude] = dLongitude; + m_dBaseValues[Latitude] = dLatitude; + m_dBaseValues[Easting] = dEasting; + m_dBaseValues[Northing] = dNorthing; + m_sUtmZone = sZone; + m_pUtmZoneComboBox->setCurrentIndex(m_pUtmZoneComboBox->findText(sZone)); + for (int nField = 0; nField < FieldCount; ++nField) { + // 正在输入的一组保留原文和光标;另一组只展示成功转换后的坐标。 + const bool bSourceField = (eSource == GeographicInput && nField < Easting) + || (eSource == ProjectedInput && nField >= Easting); + if (!bSourceField) m_pCoordinateEdits[nField]->setText(displayValue( + static_cast(nField), m_dBaseValues[nField], m_sDisplayUnits[nField])); + m_sShownTexts[nField] = m_pCoordinateEdits[nField]->text(); + } + m_bHasCoordinates = true; + m_bInputValid = true; + m_bChanged = true; + m_bRefreshing = false; } -void nmWxGeoRefDlg::loadDataToUI() +void nmWxGeoRefDlg::onCoordinateEdited(const QString&) { - m_bUpdating = true; - - // 检查UTM Zone是否有效 - QString utmZone = m_geoRefData.getUtmZone().getValue().toString(); - bool hasValidData = !utmZone.isEmpty(); - - if(hasValidData) { - // 有有效数据时,加载所有数据 - QString lonStr = QString::number(m_geoRefData.getLongitude().getValue().toDouble(), 'f', 6); - m_pLongitudeEdit->setText(lonStr); - m_lastLongitude = lonStr; - - QString latStr = QString::number(m_geoRefData.getLatitude().getValue().toDouble(), 'f', 6); - m_pLatitudeEdit->setText(latStr); - m_lastLatitude = latStr; - - int index = m_pUtmZoneCombo->findText(utmZone); - if(index >= 0) { - m_pUtmZoneCombo->setCurrentIndex(index); - } - - QString eastingStr = QString::number(m_geoRefData.getEasting().getValue().toDouble(), 'f', 2); - m_pEastingEdit->setText(eastingStr); - m_lastEasting = eastingStr; - - QString northingStr = QString::number(m_geoRefData.getNorthing().getValue().toDouble(), 'f', 2); - m_pNorthingEdit->setText(northingStr); - m_lastNorthing = northingStr; - - // 启用所有控件 - m_pLongitudeEdit->setEnabled(true); - m_pLatitudeEdit->setEnabled(true); - m_pEastingEdit->setEnabled(true); - m_pNorthingEdit->setEnabled(true); - } else { - // 无有效数据时,清空所有文本框,初始化last值 - m_pLongitudeEdit->setText(""); - m_pLatitudeEdit->setText(""); - m_pEastingEdit->setText(""); - m_pNorthingEdit->setText(""); - - m_lastLongitude = ""; - m_lastLatitude = ""; - m_lastEasting = ""; - m_lastNorthing = ""; - } - - m_bUpdating = false; + if (m_bRefreshing) return; + for (int nField = 0; nField < FieldCount; ++nField) { + if (sender() != m_pCoordinateEdits[nField]) continue; + m_eLastInput = nField < Easting ? GeographicInput : ProjectedInput; + updateCoordinates(m_eLastInput); + return; + } } -void nmWxGeoRefDlg::saveDataUI() +void nmWxGeoRefDlg::onLengthUnitChanged(int) { - // 保存经度 - nmDataAttribute longitude = m_geoRefData.getLongitude(); - double lonValue = m_pLongitudeEdit->text().toDouble(); - longitude.setValue(lonValue); - m_geoRefData.setLongitude(longitude); - - // 保存纬度 - nmDataAttribute latitude = m_geoRefData.getLatitude(); - double latValue = m_pLatitudeEdit->text().toDouble(); - latitude.setValue(latValue); - m_geoRefData.setLatitude(latitude); - - // 保存UTM Zone - nmDataAttribute utmZone = m_geoRefData.getUtmZone(); - utmZone.setValue(QVariant(m_pUtmZoneCombo->currentText())); - m_geoRefData.setUtmZone(utmZone); - - // 保存东向坐标 - nmDataAttribute easting = m_geoRefData.getEasting(); - double estValue = m_pEastingEdit->text().toDouble(); - easting.setValue(estValue); - m_geoRefData.setEasting(easting); - - // 保存北向坐标 - nmDataAttribute northing = m_geoRefData.getNorthing(); - double norValue = m_pNorthingEdit->text().toDouble(); - northing.setValue(norValue); - m_geoRefData.setNorthing(northing); + if (m_bRefreshing) return; + for (int nField = Easting; nField < FieldCount; ++nField) { + QComboBox* pUnit = m_pLengthUnits[nField]; + if (sender() != pUnit) continue; + const QString sUnit = pUnit->currentText(); + const QString sText = displayValue(static_cast(nField), m_dBaseValues[nField], sUnit); + m_bRefreshing = true; + if (!m_bInputValid || sText.isEmpty()) { + pUnit->setCurrentIndex(pUnit->findText(m_sDisplayUnits[nField])); + } else { + // 切换单位只刷新显示;不经过坐标反算,不改变工程值和修改标记。 + m_sDisplayUnits[nField] = sUnit; + m_sShownTexts[nField] = sText; + m_pCoordinateEdits[nField]->setText(sText); + } + m_bRefreshing = false; + return; + } } -void nmWxGeoRefDlg::okAccept() +void nmWxGeoRefDlg::onUtmZoneChanged(int nIndex) { + if (m_bRefreshing || nIndex < 0) return; + int nZone = 0; + char cBand = 0; + const QString sSelected = m_pUtmZoneComboBox->itemText(nIndex); + double dLatitude = 0.0, dLongitude = 0.0, dEasting = 0.0, dNorthing = 0.0; + QString sZone; + if (!parseZone(sSelected, nZone, cBand)) return; + zoneCenter(nZone, cBand, dLatitude, dLongitude); + // 沿用选带后填入区域中心的交互;四个值由同一次正投影产生,避免默认坐标相互矛盾。 + m_bInputValid = geographicToUtm(dLatitude, dLongitude, sZone, dEasting, dNorthing) + && sZone == sSelected; + if (m_bInputValid) { + m_eLastInput = NoInput; + applyCoordinates(dLongitude, dLatitude, dEasting, dNorthing, sZone, NoInput); + } + updateActions(); +} - saveDataUI(); +void nmWxGeoRefDlg::updateActions() +{ + const bool bUnits = unitsAvailable(); + // 尚未设置参考点时可直接输入经纬度,也可以先选 UTM 带再修改投影坐标。 + // 某一组输入不完整时,暂时锁住另一组,避免切换编辑来源时丢弃未修正的文本。 + const bool bGeographic = bUnits && (m_bInputValid || m_eLastInput != ProjectedInput); + const bool bProjected = bUnits && m_pUtmZoneComboBox->currentIndex() >= 0 + && (m_bInputValid || m_eLastInput != GeographicInput); + m_pCoordinateEdits[Longitude]->setEnabled(bGeographic); + m_pCoordinateEdits[Latitude]->setEnabled(bGeographic); + m_pCoordinateEdits[Easting]->setEnabled(bProjected); + m_pCoordinateEdits[Northing]->setEnabled(bProjected); + m_pLengthUnits[Easting]->setEnabled(bUnits && m_bInputValid); + m_pLengthUnits[Northing]->setEnabled(bUnits && m_bInputValid); + m_pUtmZoneComboBox->setEnabled(bUnits); + m_pOkButton->setEnabled(bUnits && m_bInputValid); +} - // 更新到全局数据管理器 - nmDataAnalyzeManager::getCurrentInstance()->updateGeoRefData(m_geoRefData); +const nmDataGeoRef& nmWxGeoRefDlg::geoRefData() const +{ + return m_oGeoRefData; +} - // 调用基类的accept - iDlgBase::accept(); +bool nmWxGeoRefDlg::hasChanges() const +{ + return m_bChanged; } -void nmWxGeoRefDlg::cancelReject() +void nmWxGeoRefDlg::accept() { - // 调用基类的reject - iDlgBase::reject(); + // 文本编辑即时检查,提交仍再次兜底;回车也不能绕过无效输入和单位服务检查。 + if (!unitsAvailable() || !updateCoordinates(m_eLastInput)) return; + if (m_bChanged) { + m_oGeoRefData.getLongitude().setValue(m_dBaseValues[Longitude]); + m_oGeoRefData.getLatitude().setValue(m_dBaseValues[Latitude]); + m_oGeoRefData.getEasting().setValue(m_dBaseValues[Easting]); + m_oGeoRefData.getNorthing().setValue(m_dBaseValues[Northing]); + m_oGeoRefData.getUtmZone().setValue(m_sUtmZone); + // 持久化仍采用度和米;显示单位仅在本次窗口中使用,不改变旧工程的读取约定。 + m_oGeoRefData.getLongitude().setUnit("o"); + m_oGeoRefData.getLatitude().setUnit("o"); + m_oGeoRefData.getEasting().setUnit("m"); + m_oGeoRefData.getNorthing().setUnit("m"); + } + iDlgBase::accept(); }