File indexing completed on 2025-01-05 03:59:02
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 <QLocale> 0014 #include <QRegularExpression> 0015 #include <QSet> 0016 0017 #include <klocalizedstring.h> 0018 0019 #include "GeoDataCoordinates.h" 0020 0021 #include "digikam_debug.h" 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, i18nc("North direction terms", "*"), 0049 placeholder, separator); 0050 //: See https://community.kde.org/Marble/GeoDataCoordinatesTranslation#Direction_terms 0051 getLocaleList(m_eastLocale, i18nc("East direction terms", "*"), 0052 placeholder, separator); 0053 //: See https://community.kde.org/Marble/GeoDataCoordinatesTranslation#Direction_terms 0054 getLocaleList(m_southLocale, i18nc("South direction terms", "*"), 0055 placeholder, separator); 0056 //: See https://community.kde.org/Marble/GeoDataCoordinatesTranslation#Direction_terms 0057 getLocaleList(m_westLocale, i18nc("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, i18nc("Degree symbol terms", "*"), 0096 placeholder, separator); 0097 //: See https://community.kde.org/Marble/GeoDataCoordinatesTranslation#Coordinate_symbols 0098 getLocaleList(m_minutesLocale, i18nc("Minutes symbol terms", "*"), 0099 placeholder, separator); 0100 //: See https://community.kde.org/Marble/GeoDataCoordinatesTranslation#Coordinate_symbols 0101 getLocaleList(m_secondsLocale, i18nc("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 // qCWarning(DIGIKAM_MARBLE_LOG) << regex.isValid() << regex.errorString() << regex.pattern(); 0296 QRegularExpressionMatch match = regex.match(input); 0297 if (!match.hasMatch()) { 0298 // qCWarning(DIGIKAM_MARBLE_LOG) << "LonLatParser::tryMatchFromD -> no match"; 0299 return false; 0300 } 0301 // qCWarning(DIGIKAM_MARBLE_LOG) << "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 QString decimalPoint = QLocale::system().decimalPoint(); 0336 0337 return (decimalPoint == QLatin1String(".")) ? 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, Qt::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 }