File indexing completed on 2024-05-12 03:50:19

0001 // SPDX-License-Identifier: LGPL-2.1-or-later
0002 //
0003 // SPDX-FileCopyrightText: 2004-2007 Torsten Rahn <tackat@kde.org>
0004 // SPDX-FileCopyrightText: 2007-2008 Inge Wallin <ingwa@kde.org>
0005 // SPDX-FileCopyrightText: 2008 Patrick Spendrin <ps_ml@gmx.de>
0006 // SPDX-FileCopyrightText: 2011 Friedrich W. H. Kossebau <kossebau@kde.org>
0007 // SPDX-FileCopyrightText: 2011 Bernhard Beschow <bbeschow@cs.tu-berlin.de>
0008 // SPDX-FileCopyrightText: 2015 Alejandro Garcia Montoro <alejandro.garciamontoro@gmail.com>
0009 //
0010 
0011 #include "LonLatParser_p.h"
0012 
0013 #include "GeoDataCoordinates.h"
0014 
0015 #include "MarbleDebug.h"
0016 
0017 #include <QLocale>
0018 #include <QRegularExpression>
0019 #include <QSet>
0020 
0021 
0022 namespace Marble
0023 {
0024 
0025 LonLatParser::LonLatParser()
0026     : m_lon(0.0)
0027     , m_lat(0.0)
0028     , m_north(QStringLiteral("n"))
0029     , m_east( QStringLiteral("e"))
0030     , m_south(QStringLiteral("s"))
0031     , m_west( QStringLiteral("w"))
0032     , m_decimalPointExp(createDecimalPointExp())
0033 {
0034 }
0035 
0036 
0037 void LonLatParser::initAll()
0038 {
0039     // already all initialized?
0040     if (! m_dirCapExp.isEmpty()) {
0041         return;
0042     }
0043 
0044     const QLatin1String placeholder = QLatin1String("*");
0045     const QString separator = QStringLiteral("|");
0046 
0047     //: See https://community.kde.org/Marble/GeoDataCoordinatesTranslation#Direction_terms
0048     getLocaleList(m_northLocale, GeoDataCoordinates::tr("*", "North direction terms"),
0049                   placeholder, separator);
0050     //: See https://community.kde.org/Marble/GeoDataCoordinatesTranslation#Direction_terms
0051     getLocaleList(m_eastLocale, GeoDataCoordinates::tr("*", "East direction terms"),
0052                   placeholder, separator);
0053     //: See https://community.kde.org/Marble/GeoDataCoordinatesTranslation#Direction_terms
0054     getLocaleList(m_southLocale, GeoDataCoordinates::tr("*", "South direction terms"),
0055                   placeholder, separator);
0056     //: See https://community.kde.org/Marble/GeoDataCoordinatesTranslation#Direction_terms
0057     getLocaleList(m_westLocale, GeoDataCoordinates::tr("*", "West direction terms"),
0058                   placeholder, separator);
0059 
0060     // use a set to remove duplicates
0061     QSet<QString> dirs = QSet<QString>()
0062         << m_north << m_east << m_south << m_west;
0063 #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
0064     dirs += QSet<QString>( m_northLocale.begin(), m_northLocale.end() );
0065     dirs += QSet<QString>( m_eastLocale.begin(), m_eastLocale.end() );
0066     dirs += QSet<QString>( m_southLocale.begin(), m_southLocale.end() );
0067     dirs += QSet<QString>( m_westLocale.begin(), m_westLocale.end() );
0068 #else
0069     dirs += m_northLocale.toSet();
0070     dirs += m_eastLocale.toSet();
0071     dirs += m_southLocale.toSet();
0072     dirs += m_westLocale.toSet();
0073 #endif
0074 
0075     QString fullNamesExp;
0076     QString simpleLetters;
0077 
0078     for(const QString& dir: dirs) {
0079         // collect simple letters
0080         if ((dir.length() == 1) && (QLatin1Char('a')<=dir.at(0)) && (dir.at(0)<=QLatin1Char('z'))) {
0081             simpleLetters += dir;
0082             continue;
0083         }
0084 
0085         // okay to add '|' also for last, separates from firstLetters
0086         fullNamesExp += QRegularExpression::escape(dir) + QLatin1Char('|');
0087     }
0088 
0089     // Sets "(north|east|south|west|[nesw])" in en, as translated names match untranslated ones
0090     m_dirCapExp =
0091         QLatin1Char('(') + fullNamesExp + QLatin1Char('[') + simpleLetters + QLatin1String("])");
0092 
0093     // expressions for symbols of degree, minutes and seconds
0094     //: See https://community.kde.org/Marble/GeoDataCoordinatesTranslation#Coordinate_symbols
0095     getLocaleList(m_degreeLocale, GeoDataCoordinates::tr("*", "Degree symbol terms"),
0096                   placeholder, separator);
0097     //: See https://community.kde.org/Marble/GeoDataCoordinatesTranslation#Coordinate_symbols
0098     getLocaleList(m_minutesLocale, GeoDataCoordinates::tr("*", "Minutes symbol terms"),
0099                   placeholder, separator);
0100     //: See https://community.kde.org/Marble/GeoDataCoordinatesTranslation#Coordinate_symbols
0101     getLocaleList(m_secondsLocale, GeoDataCoordinates::tr("*", "Seconds symbol terms"),
0102                   placeholder, separator);
0103 
0104     // Used unicode chars:
0105     // u00B0: ° DEGREE SIGN
0106     // u00BA: º MASCULINE ORDINAL INDICATOR (found used as degree sign)
0107     // u2032: ′ PRIME (minutes)
0108     // u00B4: ´ ACUTE ACCENT (found as minutes sign)
0109     // u02CA: ˊ MODIFIER LETTER ACUTE ACCENT
0110     // u2019: ’ RIGHT SINGLE QUOTATION MARK
0111     // u2033: ″ DOUBLE PRIME (seconds)
0112     // u201D: ” RIGHT DOUBLE QUOTATION MARK
0113 
0114     m_degreeExp = QStringLiteral(u"\u00B0|\u00BA");
0115     for(const QString& symbol: m_degreeLocale) {
0116         m_degreeExp += QLatin1Char('|') + QRegularExpression::escape(symbol);
0117     }
0118     m_minutesExp = QStringLiteral(u"'|\u2032|\u00B4|\u20C2|\u2019");
0119     for(const QString& symbol: m_minutesLocale) {
0120         m_minutesExp += QLatin1Char('|') + QRegularExpression::escape(symbol);
0121     }
0122     m_secondsExp = QStringLiteral(u"\"|\u2033|\u201D|''|\u2032\u2032|\u00B4\u00B4|\u20C2\u20C2|\u2019\u2019");
0123     for(const QString& symbol: m_secondsLocale) {
0124         m_secondsExp += QLatin1Char('|') + QRegularExpression::escape(symbol);
0125     }
0126 }
0127 
0128 bool LonLatParser::parse(const QString& string)
0129 {
0130     const QString input = string.toLower().trimmed();
0131 
0132     // #1: Just two numbers, no directions, e.g. 74.2245 -32.2434 (assumes lat lon)
0133     {
0134         const QString numberCapExp = QStringLiteral("\\A(?:") +
0135             QStringLiteral("([-+]?\\d{1,3}%1?\\d*(?:[eE][+-]?\\d+)?)(?:,|;|\\s)\\s*").arg(m_decimalPointExp) +
0136             QStringLiteral("([-+]?\\d{1,3}%1?\\d*(?:[eE][+-]?\\d+)?)").arg(m_decimalPointExp) +
0137             QStringLiteral(")\\z");
0138 
0139         const QRegularExpression regex(numberCapExp);
0140         QRegularExpressionMatch match = regex.match(input);
0141         if (match.hasMatch()) {
0142             m_lon = parseDouble(match.captured(2));
0143             m_lat = parseDouble(match.captured(1));
0144 
0145             return true;
0146         }
0147     }
0148 
0149     initAll();
0150 
0151     if (tryMatchFromD(input, PostfixDir)) {
0152         return true;
0153     }
0154 
0155     if (tryMatchFromD(input, PrefixDir)) {
0156         return true;
0157     }
0158 
0159     if (tryMatchFromDms(input, PostfixDir)) {
0160         return true;
0161     }
0162 
0163     if (tryMatchFromDms(input, PrefixDir)) {
0164         return true;
0165     }
0166 
0167     if (tryMatchFromDm(input, PostfixDir)) {
0168         return true;
0169     }
0170 
0171     if (tryMatchFromDm(input, PrefixDir)) {
0172         return true;
0173     }
0174 
0175     return false;
0176 }
0177 
0178 // #3: Sexagesimal
0179 bool LonLatParser::tryMatchFromDms(const QString& input, DirPosition dirPosition)
0180 {
0181     // direction as postfix
0182     const QString postfixCapExp = QStringLiteral("\\A(?:") +
0183         QStringLiteral("([-+]?)(\\d{1,3})(?:%3|\\s)\\s*(\\d{1,2})(?:%4|\\s)\\s*") +
0184         QStringLiteral("(\\d{1,2}%1?\\d*)(?:%5)?\\s*%2[,;]?\\s*") +
0185         QStringLiteral("([-+]?)(\\d{1,3})(?:%3|\\s)\\s*(\\d{1,2})(?:%4|\\s)\\s*") +
0186         QStringLiteral("(\\d{1,2}%1?\\d*)(?:%5)?\\s*%2") +
0187         QStringLiteral(")\\z");
0188 
0189     // direction as prefix
0190     const QString prefixCapExp = QStringLiteral("\\A(?:") +
0191         QStringLiteral("%2\\s*([-+]?)(\\d{1,3})(?:%3|\\s)\\s*(\\d{1,2})(?:%4|\\s)\\s*") +
0192         QStringLiteral("(\\d{1,2}%1?\\d*)(?:%5)?\\s*(?:,|;|\\s)\\s*") +
0193         QStringLiteral("%2\\s*([-+]?)(\\d{1,3})(?:%3|\\s)\\s*(\\d{1,2})(?:%4|\\s)\\s*") +
0194         QStringLiteral("(\\d{1,2}%1?\\d*)(?:%5)?") +
0195         QStringLiteral(")\\z");
0196 
0197     const QString &expTemplate = (dirPosition == PostfixDir) ? postfixCapExp
0198                                                              : prefixCapExp;
0199 
0200     const QString numberCapExp = expTemplate.arg(m_decimalPointExp, m_dirCapExp,
0201                                                  m_degreeExp, m_minutesExp, m_secondsExp);
0202 
0203     const QRegularExpression regex(numberCapExp);
0204     QRegularExpressionMatch match = regex.match(input);
0205     if (!match.hasMatch()) {
0206         return false;
0207     }
0208 
0209     bool isDir1LonDir;
0210     bool isLonDirPosHemisphere;
0211     bool isLatDirPosHemisphere;
0212     const QString dir1 = match.captured(dirPosition == PostfixDir ? 5 : 1);
0213     const QString dir2 = match.captured(dirPosition == PostfixDir ? 10 : 6);
0214     if (!isCorrectDirections(dir1, dir2, isDir1LonDir,
0215                              isLonDirPosHemisphere, isLatDirPosHemisphere)) {
0216         return false;
0217     }
0218 
0219     const int valueStartIndex1 = (dirPosition == PostfixDir ? 1 : 2);
0220     const int valueStartIndex2 = (dirPosition == PostfixDir ? 6 : 7);
0221     m_lon = degreeValueFromDMS(match, isDir1LonDir ? valueStartIndex1 : valueStartIndex2,
0222                                isLonDirPosHemisphere);
0223     m_lat = degreeValueFromDMS(match, isDir1LonDir ? valueStartIndex2 : valueStartIndex1,
0224                                isLatDirPosHemisphere);
0225 
0226     return true;
0227 }
0228 
0229 // #4: Sexagesimal with minute precision
0230 bool LonLatParser::tryMatchFromDm(const QString& input, DirPosition dirPosition)
0231 {
0232     // direction as postfix
0233     const QString postfixCapExp = QStringLiteral("\\A(?:") +
0234         QStringLiteral("([-+]?)(\\d{1,3})(?:%3|\\s)\\s*(\\d{1,2}%1?\\d*)(?:%4)?\\s*%2[,;]?\\s*") +
0235         QStringLiteral("([-+]?)(\\d{1,3})(?:%3|\\s)\\s*(\\d{1,2}%1?\\d*)(?:%4)?\\s*%2") +
0236         QStringLiteral(")\\z");
0237 
0238     // direction as prefix
0239     const QString prefixCapExp = QStringLiteral("\\A(?:") +
0240         QStringLiteral("%2\\s*([-+]?)(\\d{1,3})(?:%3|\\s)\\s*(\\d{1,2}%1?\\d*)(?:%4)?\\s*(?:,|;|\\s)\\s*") +
0241         QStringLiteral("%2\\s*([-+]?)(\\d{1,3})(?:%3|\\s)\\s*(\\d{1,2}%1?\\d*)(?:%4)?") +
0242         QStringLiteral(")\\z");
0243 
0244     const QString& expTemplate = (dirPosition == PostfixDir) ? postfixCapExp
0245                                                              : prefixCapExp;
0246 
0247     const QString numberCapExp = expTemplate.arg(m_decimalPointExp, m_dirCapExp,
0248                                                   m_degreeExp, m_minutesExp);
0249     const QRegularExpression regex(numberCapExp);
0250     QRegularExpressionMatch match = regex.match(input);
0251     if (!match.hasMatch()) {
0252         return false;
0253     }
0254 
0255     bool isDir1LonDir;
0256     bool isLonDirPosHemisphere;
0257     bool isLatDirPosHemisphere;
0258     const QString dir1 = match.captured(dirPosition == PostfixDir ? 4 : 1);
0259     const QString dir2 = match.captured(dirPosition == PostfixDir ? 8 : 5);
0260     if (!isCorrectDirections(dir1, dir2, isDir1LonDir,
0261                              isLonDirPosHemisphere, isLatDirPosHemisphere)) {
0262         return false;
0263     }
0264 
0265     const int valueStartIndex1 = (dirPosition == PostfixDir ? 1 : 2);
0266     const int valueStartIndex2 = (dirPosition == PostfixDir ? 5 : 6);
0267     m_lon = degreeValueFromDM(match, isDir1LonDir ? valueStartIndex1 : valueStartIndex2,
0268                               isLonDirPosHemisphere);
0269     m_lat = degreeValueFromDM(match, isDir1LonDir ? valueStartIndex2 : valueStartIndex1,
0270                               isLatDirPosHemisphere);
0271 
0272     return true;
0273 }
0274 
0275 // #2: Two numbers with directions
0276 bool LonLatParser::tryMatchFromD(const QString& input, DirPosition dirPosition)
0277 {
0278     // direction as postfix, e.g. 74.2245 N 32.2434 W
0279     const QString postfixCapExp = QStringLiteral("\\A(?:") +
0280         QStringLiteral("([-+]?\\d{1,3}%1?\\d*)(?:%3)?(?:\\s*)%2(?:,|;|\\s)\\s*") +
0281         QStringLiteral("([-+]?\\d{1,3}%1?\\d*)(?:%3)?(?:\\s*)%2") +
0282         QStringLiteral(")\\z");
0283 
0284     // direction as prefix, e.g. N 74.2245 W 32.2434
0285     const QString prefixCapExp = QStringLiteral("\\A(?:") +
0286         QStringLiteral("%2\\s*([-+]?\\d{1,3}%1?\\d*)(?:%3)?\\s*(?:,|;|\\s)\\s*") +
0287         QStringLiteral("%2\\s*([-+]?\\d{1,3}%1?\\d*)(?:%3)?") +
0288         QStringLiteral(")\\z");
0289 
0290     const QString& expTemplate = (dirPosition == PostfixDir) ? postfixCapExp
0291                                                              : prefixCapExp;
0292 
0293     const QString numberCapExp = expTemplate.arg(m_decimalPointExp, m_dirCapExp, m_degreeExp);
0294     const QRegularExpression regex(numberCapExp);
0295 // qWarning() << regex.isValid() << regex.errorString() << regex.pattern();
0296     QRegularExpressionMatch match = regex.match(input);
0297     if (!match.hasMatch()) {
0298 //         qWarning() << "LonLatParser::tryMatchFromD -> no match";
0299         return false;
0300     }
0301 //     qWarning() << "LonLatParser::tryMatchFromD -> match" << match;
0302 
0303     bool isDir1LonDir;
0304     bool isLonDirPosHemisphere;
0305     bool isLatDirPosHemisphere;
0306     const QString dir1 = match.captured(dirPosition == PostfixDir ? 2 : 1);
0307     const QString dir2 = match.captured(dirPosition == PostfixDir ? 4 : 3);
0308     if (!isCorrectDirections(dir1, dir2, isDir1LonDir,
0309                              isLonDirPosHemisphere, isLatDirPosHemisphere)) {
0310         return false;
0311     }
0312 
0313     const int valueStartIndex1 = (dirPosition == PostfixDir ? 1 : 2);
0314     const int valueStartIndex2 = (dirPosition == PostfixDir ? 3 : 4);
0315     m_lon = degreeValueFromD(match, isDir1LonDir ? valueStartIndex1 : valueStartIndex2,
0316                              isLonDirPosHemisphere);
0317     m_lat = degreeValueFromD(match, isDir1LonDir ? valueStartIndex2 : valueStartIndex1,
0318                              isLatDirPosHemisphere);
0319 
0320     return true;
0321 }
0322 
0323 double LonLatParser::parseDouble(const QString& input)
0324 {
0325     // Decide by decimalpoint if system locale or C locale should be tried.
0326     // Otherwise if first trying with a system locale when the string is in C locale,
0327     // the "." might be misinterpreted as thousands group separator and thus a wrong
0328     // value yielded
0329     QLocale locale = QLocale::system();
0330     return input.contains(locale.decimalPoint()) ? locale.toDouble(input) : input.toDouble();
0331 }
0332 
0333 QString LonLatParser::createDecimalPointExp()
0334 {
0335     const QChar decimalPoint = QLocale::system().decimalPoint();
0336 
0337     return (decimalPoint == QLatin1Char('.')) ? QStringLiteral("\\.") :
0338         QLatin1String("[.") + decimalPoint + QLatin1Char(']');
0339 }
0340 
0341 void LonLatParser::getLocaleList(QStringList& localeList, const QString& localeListString,
0342                                  const QLatin1String& placeholder, const QString& separator)
0343 {
0344     const QString lowerLocaleListString = localeListString.toLower();
0345     if (lowerLocaleListString != placeholder) {
0346         localeList = lowerLocaleListString.split(separator, QString::SkipEmptyParts);
0347     }
0348 }
0349 
0350 bool LonLatParser::isDirection(const QString& input, const QStringList& directions)
0351 {
0352     return (directions.contains(input));
0353 }
0354 
0355 bool LonLatParser::isDirection(const QString& input, const QString& direction)
0356 {
0357     return (input == direction);
0358 }
0359 
0360 bool LonLatParser::isOneOfDirections(const QString& input,
0361                                      const QString& firstDirection,
0362                                      const QString& secondDirection,
0363                                      bool& isFirstDirection)
0364 {
0365     isFirstDirection = isDirection(input, firstDirection);
0366     return isFirstDirection || isDirection(input, secondDirection);
0367 }
0368 
0369 bool LonLatParser::isOneOfDirections(const QString& input,
0370                                      const QStringList& firstDirections,
0371                                      const QStringList& secondDirections,
0372                                      bool& isFirstDirection)
0373 {
0374     isFirstDirection = isDirection(input, firstDirections);
0375     return isFirstDirection || isDirection(input, secondDirections);
0376 }
0377 
0378 
0379 bool LonLatParser::isLocaleLonDirection(const QString& input,
0380                                         bool& isDirPosHemisphere) const
0381 {
0382     return isOneOfDirections(input, m_eastLocale, m_westLocale, isDirPosHemisphere);
0383 }
0384 
0385 bool LonLatParser::isLocaleLatDirection(const QString& input,
0386                                         bool& isDirPosHemisphere) const
0387 {
0388     return isOneOfDirections(input, m_northLocale, m_southLocale, isDirPosHemisphere);
0389 }
0390 
0391 bool LonLatParser::isLonDirection(const QString& input,
0392                                   bool& isDirPosHemisphere) const
0393 {
0394     return isOneOfDirections(input, m_east, m_west, isDirPosHemisphere);
0395 }
0396 
0397 bool LonLatParser::isLatDirection(const QString& input,
0398                                   bool& isDirPosHemisphere) const
0399 {
0400     return isOneOfDirections(input, m_north, m_south, isDirPosHemisphere);
0401 }
0402 
0403 
0404 qreal LonLatParser::degreeValueFromDMS(const QRegularExpressionMatch& regexMatch, int c, bool isPosHemisphere)
0405 {
0406     const bool isNegativeValue = (regexMatch.captured(c++) == QLatin1String("-"));
0407     const uint degree = regexMatch.captured(c++).toUInt();
0408     const uint minutes = regexMatch.captured(c++).toUInt();
0409     const qreal seconds = parseDouble(regexMatch.captured(c));
0410 
0411     qreal result = degree + (minutes * MIN2HOUR) + (seconds * SEC2HOUR);
0412 
0413     if (isNegativeValue) {
0414         result *= -1;
0415     }
0416     if (! isPosHemisphere) {
0417         result *= -1;
0418     }
0419 
0420     return result;
0421 }
0422 
0423 qreal LonLatParser::degreeValueFromDM(const QRegularExpressionMatch& regexMatch, int c, bool isPosHemisphere)
0424 {
0425     const bool isNegativeValue = (regexMatch.captured(c++) == QLatin1String("-"));
0426     const uint degree = regexMatch.captured(c++).toUInt();
0427     const qreal minutes = parseDouble(regexMatch.captured(c));
0428 
0429     qreal result = degree + (minutes * MIN2HOUR);
0430 
0431     if (isNegativeValue) {
0432         result *= -1;
0433     }
0434     if (! isPosHemisphere) {
0435         result *= -1;
0436     }
0437 
0438     return result;
0439 }
0440 
0441 qreal LonLatParser::degreeValueFromD(const QRegularExpressionMatch& regexMatch, int c, bool isPosHemisphere)
0442 {
0443     qreal result = parseDouble(regexMatch.captured(c));
0444 
0445     if (! isPosHemisphere) {
0446         result *= -1;
0447     }
0448 
0449     return result;
0450 }
0451 
0452 bool LonLatParser::isCorrectDirections(const QString& dir1, const QString& dir2,
0453                                        bool& isDir1LonDir,
0454                                        bool& isLonDirPosHemisphere,
0455                                        bool& isLatDirPosHemisphere) const
0456 {
0457     // first try localized names
0458     isDir1LonDir = isLocaleLonDirection(dir1, isLonDirPosHemisphere);
0459     const bool resultLocale = isDir1LonDir ?
0460         isLocaleLatDirection(dir2, isLatDirPosHemisphere) :
0461         (isLocaleLatDirection(dir1, isLatDirPosHemisphere) &&
0462          isLocaleLonDirection(dir2, isLonDirPosHemisphere));
0463 
0464     if (resultLocale) {
0465         return resultLocale;
0466     }
0467 
0468     // fallback to try english names as lingua franca
0469     isDir1LonDir = isLonDirection(dir1, isLonDirPosHemisphere);
0470     return isDir1LonDir ?
0471         isLatDirection(dir2, isLatDirPosHemisphere) :
0472         (isLatDirection(dir1, isLatDirPosHemisphere) &&
0473          isLonDirection(dir2, isLonDirPosHemisphere));
0474 }
0475 
0476 }