Marble

LonLatParser.cpp
1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2004-2007 Torsten Rahn <[email protected]>
4 // SPDX-FileCopyrightText: 2007-2008 Inge Wallin <[email protected]>
5 // SPDX-FileCopyrightText: 2008 Patrick Spendrin <[email protected]>
6 // SPDX-FileCopyrightText: 2011 Friedrich W. H. Kossebau <[email protected]>
7 // SPDX-FileCopyrightText: 2011 Bernhard Beschow <[email protected]>
8 // SPDX-FileCopyrightText: 2015 Alejandro Garcia Montoro <[email protected]>
9 //
10 
11 #include "LonLatParser_p.h"
12 
13 #include "GeoDataCoordinates.h"
14 
15 #include "MarbleDebug.h"
16 
17 #include <QLocale>
18 #include <QRegularExpression>
19 #include <QSet>
20 
21 
22 namespace Marble
23 {
24 
25 LonLatParser::LonLatParser()
26  : m_lon(0.0)
27  , m_lat(0.0)
28  , m_north(QStringLiteral("n"))
29  , m_east( QStringLiteral("e"))
30  , m_south(QStringLiteral("s"))
31  , m_west( QStringLiteral("w"))
32  , m_decimalPointExp(createDecimalPointExp())
33 {
34 }
35 
36 
37 void LonLatParser::initAll()
38 {
39  // already all initialized?
40  if (! m_dirCapExp.isEmpty()) {
41  return;
42  }
43 
44  const QLatin1String placeholder = QLatin1String("*");
45  const QString separator = QStringLiteral("|");
46 
47  //: See https://community.kde.org/Marble/GeoDataCoordinatesTranslation#Direction_terms
48  getLocaleList(m_northLocale, GeoDataCoordinates::tr("*", "North direction terms"),
49  placeholder, separator);
50  //: See https://community.kde.org/Marble/GeoDataCoordinatesTranslation#Direction_terms
51  getLocaleList(m_eastLocale, GeoDataCoordinates::tr("*", "East direction terms"),
52  placeholder, separator);
53  //: See https://community.kde.org/Marble/GeoDataCoordinatesTranslation#Direction_terms
54  getLocaleList(m_southLocale, GeoDataCoordinates::tr("*", "South direction terms"),
55  placeholder, separator);
56  //: See https://community.kde.org/Marble/GeoDataCoordinatesTranslation#Direction_terms
57  getLocaleList(m_westLocale, GeoDataCoordinates::tr("*", "West direction terms"),
58  placeholder, separator);
59 
60  // use a set to remove duplicates
62  << m_north << m_east << m_south << m_west;
63 #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
64  dirs += QSet<QString>( m_northLocale.begin(), m_northLocale.end() );
65  dirs += QSet<QString>( m_eastLocale.begin(), m_eastLocale.end() );
66  dirs += QSet<QString>( m_southLocale.begin(), m_southLocale.end() );
67  dirs += QSet<QString>( m_westLocale.begin(), m_westLocale.end() );
68 #else
69  dirs += m_northLocale.toSet();
70  dirs += m_eastLocale.toSet();
71  dirs += m_southLocale.toSet();
72  dirs += m_westLocale.toSet();
73 #endif
74 
75  QString fullNamesExp;
76  QString simpleLetters;
77 
78  for(const QString& dir: dirs) {
79  // collect simple letters
80  if ((dir.length() == 1) && (QLatin1Char('a')<=dir.at(0)) && (dir.at(0)<=QLatin1Char('z'))) {
81  simpleLetters += dir;
82  continue;
83  }
84 
85  // okay to add '|' also for last, separates from firstLetters
86  fullNamesExp += QRegularExpression::escape(dir) + QLatin1Char('|');
87  }
88 
89  // Sets "(north|east|south|west|[nesw])" in en, as translated names match untranslated ones
90  m_dirCapExp =
91  QLatin1Char('(') + fullNamesExp + QLatin1Char('[') + simpleLetters + QLatin1String("])");
92 
93  // expressions for symbols of degree, minutes and seconds
94  //: See https://community.kde.org/Marble/GeoDataCoordinatesTranslation#Coordinate_symbols
95  getLocaleList(m_degreeLocale, GeoDataCoordinates::tr("*", "Degree symbol terms"),
96  placeholder, separator);
97  //: See https://community.kde.org/Marble/GeoDataCoordinatesTranslation#Coordinate_symbols
98  getLocaleList(m_minutesLocale, GeoDataCoordinates::tr("*", "Minutes symbol terms"),
99  placeholder, separator);
100  //: See https://community.kde.org/Marble/GeoDataCoordinatesTranslation#Coordinate_symbols
101  getLocaleList(m_secondsLocale, GeoDataCoordinates::tr("*", "Seconds symbol terms"),
102  placeholder, separator);
103 
104  // Used unicode chars:
105  // u00B0: ° DEGREE SIGN
106  // u00BA: º MASCULINE ORDINAL INDICATOR (found used as degree sign)
107  // u2032: ′ PRIME (minutes)
108  // u00B4: ´ ACUTE ACCENT (found as minutes sign)
109  // u02CA: ˊ MODIFIER LETTER ACUTE ACCENT
110  // u2019: ’ RIGHT SINGLE QUOTATION MARK
111  // u2033: ″ DOUBLE PRIME (seconds)
112  // u201D: ” RIGHT DOUBLE QUOTATION MARK
113 
114  m_degreeExp = QStringLiteral(u"\u00B0|\u00BA");
115  for(const QString& symbol: m_degreeLocale) {
116  m_degreeExp += QLatin1Char('|') + QRegularExpression::escape(symbol);
117  }
118  m_minutesExp = QStringLiteral(u"'|\u2032|\u00B4|\u20C2|\u2019");
119  for(const QString& symbol: m_minutesLocale) {
120  m_minutesExp += QLatin1Char('|') + QRegularExpression::escape(symbol);
121  }
122  m_secondsExp = QStringLiteral(u"\"|\u2033|\u201D|''|\u2032\u2032|\u00B4\u00B4|\u20C2\u20C2|\u2019\u2019");
123  for(const QString& symbol: m_secondsLocale) {
124  m_secondsExp += QLatin1Char('|') + QRegularExpression::escape(symbol);
125  }
126 }
127 
128 bool LonLatParser::parse(const QString& string)
129 {
130  const QString input = string.toLower().trimmed();
131 
132  // #1: Just two numbers, no directions, e.g. 74.2245 -32.2434 (assumes lat lon)
133  {
134  const QString numberCapExp = QStringLiteral("\\A(?:") +
135  QStringLiteral("([-+]?\\d{1,3}%1?\\d*(?:[eE][+-]?\\d+)?)(?:,|;|\\s)\\s*").arg(m_decimalPointExp) +
136  QStringLiteral("([-+]?\\d{1,3}%1?\\d*(?:[eE][+-]?\\d+)?)").arg(m_decimalPointExp) +
137  QStringLiteral(")\\z");
138 
139  const QRegularExpression regex(numberCapExp);
140  QRegularExpressionMatch match = regex.match(input);
141  if (match.hasMatch()) {
142  m_lon = parseDouble(match.captured(2));
143  m_lat = parseDouble(match.captured(1));
144 
145  return true;
146  }
147  }
148 
149  initAll();
150 
151  if (tryMatchFromD(input, PostfixDir)) {
152  return true;
153  }
154 
155  if (tryMatchFromD(input, PrefixDir)) {
156  return true;
157  }
158 
159  if (tryMatchFromDms(input, PostfixDir)) {
160  return true;
161  }
162 
163  if (tryMatchFromDms(input, PrefixDir)) {
164  return true;
165  }
166 
167  if (tryMatchFromDm(input, PostfixDir)) {
168  return true;
169  }
170 
171  if (tryMatchFromDm(input, PrefixDir)) {
172  return true;
173  }
174 
175  return false;
176 }
177 
178 // #3: Sexagesimal
179 bool LonLatParser::tryMatchFromDms(const QString& input, DirPosition dirPosition)
180 {
181  // direction as postfix
182  const QString postfixCapExp = QStringLiteral("\\A(?:") +
183  QStringLiteral("([-+]?)(\\d{1,3})(?:%3|\\s)\\s*(\\d{1,2})(?:%4|\\s)\\s*") +
184  QStringLiteral("(\\d{1,2}%1?\\d*)(?:%5)?\\s*%2[,;]?\\s*") +
185  QStringLiteral("([-+]?)(\\d{1,3})(?:%3|\\s)\\s*(\\d{1,2})(?:%4|\\s)\\s*") +
186  QStringLiteral("(\\d{1,2}%1?\\d*)(?:%5)?\\s*%2") +
187  QStringLiteral(")\\z");
188 
189  // direction as prefix
190  const QString prefixCapExp = QStringLiteral("\\A(?:") +
191  QStringLiteral("%2\\s*([-+]?)(\\d{1,3})(?:%3|\\s)\\s*(\\d{1,2})(?:%4|\\s)\\s*") +
192  QStringLiteral("(\\d{1,2}%1?\\d*)(?:%5)?\\s*(?:,|;|\\s)\\s*") +
193  QStringLiteral("%2\\s*([-+]?)(\\d{1,3})(?:%3|\\s)\\s*(\\d{1,2})(?:%4|\\s)\\s*") +
194  QStringLiteral("(\\d{1,2}%1?\\d*)(?:%5)?") +
195  QStringLiteral(")\\z");
196 
197  const QString &expTemplate = (dirPosition == PostfixDir) ? postfixCapExp
198  : prefixCapExp;
199 
200  const QString numberCapExp = expTemplate.arg(m_decimalPointExp, m_dirCapExp,
201  m_degreeExp, m_minutesExp, m_secondsExp);
202 
203  const QRegularExpression regex(numberCapExp);
204  QRegularExpressionMatch match = regex.match(input);
205  if (!match.hasMatch()) {
206  return false;
207  }
208 
209  bool isDir1LonDir;
210  bool isLonDirPosHemisphere;
211  bool isLatDirPosHemisphere;
212  const QString dir1 = match.captured(dirPosition == PostfixDir ? 5 : 1);
213  const QString dir2 = match.captured(dirPosition == PostfixDir ? 10 : 6);
214  if (!isCorrectDirections(dir1, dir2, isDir1LonDir,
215  isLonDirPosHemisphere, isLatDirPosHemisphere)) {
216  return false;
217  }
218 
219  const int valueStartIndex1 = (dirPosition == PostfixDir ? 1 : 2);
220  const int valueStartIndex2 = (dirPosition == PostfixDir ? 6 : 7);
221  m_lon = degreeValueFromDMS(match, isDir1LonDir ? valueStartIndex1 : valueStartIndex2,
222  isLonDirPosHemisphere);
223  m_lat = degreeValueFromDMS(match, isDir1LonDir ? valueStartIndex2 : valueStartIndex1,
224  isLatDirPosHemisphere);
225 
226  return true;
227 }
228 
229 // #4: Sexagesimal with minute precision
230 bool LonLatParser::tryMatchFromDm(const QString& input, DirPosition dirPosition)
231 {
232  // direction as postfix
233  const QString postfixCapExp = QStringLiteral("\\A(?:") +
234  QStringLiteral("([-+]?)(\\d{1,3})(?:%3|\\s)\\s*(\\d{1,2}%1?\\d*)(?:%4)?\\s*%2[,;]?\\s*") +
235  QStringLiteral("([-+]?)(\\d{1,3})(?:%3|\\s)\\s*(\\d{1,2}%1?\\d*)(?:%4)?\\s*%2") +
236  QStringLiteral(")\\z");
237 
238  // direction as prefix
239  const QString prefixCapExp = QStringLiteral("\\A(?:") +
240  QStringLiteral("%2\\s*([-+]?)(\\d{1,3})(?:%3|\\s)\\s*(\\d{1,2}%1?\\d*)(?:%4)?\\s*(?:,|;|\\s)\\s*") +
241  QStringLiteral("%2\\s*([-+]?)(\\d{1,3})(?:%3|\\s)\\s*(\\d{1,2}%1?\\d*)(?:%4)?") +
242  QStringLiteral(")\\z");
243 
244  const QString& expTemplate = (dirPosition == PostfixDir) ? postfixCapExp
245  : prefixCapExp;
246 
247  const QString numberCapExp = expTemplate.arg(m_decimalPointExp, m_dirCapExp,
248  m_degreeExp, m_minutesExp);
249  const QRegularExpression regex(numberCapExp);
250  QRegularExpressionMatch match = regex.match(input);
251  if (!match.hasMatch()) {
252  return false;
253  }
254 
255  bool isDir1LonDir;
256  bool isLonDirPosHemisphere;
257  bool isLatDirPosHemisphere;
258  const QString dir1 = match.captured(dirPosition == PostfixDir ? 4 : 1);
259  const QString dir2 = match.captured(dirPosition == PostfixDir ? 8 : 5);
260  if (!isCorrectDirections(dir1, dir2, isDir1LonDir,
261  isLonDirPosHemisphere, isLatDirPosHemisphere)) {
262  return false;
263  }
264 
265  const int valueStartIndex1 = (dirPosition == PostfixDir ? 1 : 2);
266  const int valueStartIndex2 = (dirPosition == PostfixDir ? 5 : 6);
267  m_lon = degreeValueFromDM(match, isDir1LonDir ? valueStartIndex1 : valueStartIndex2,
268  isLonDirPosHemisphere);
269  m_lat = degreeValueFromDM(match, isDir1LonDir ? valueStartIndex2 : valueStartIndex1,
270  isLatDirPosHemisphere);
271 
272  return true;
273 }
274 
275 // #2: Two numbers with directions
276 bool LonLatParser::tryMatchFromD(const QString& input, DirPosition dirPosition)
277 {
278  // direction as postfix, e.g. 74.2245 N 32.2434 W
279  const QString postfixCapExp = QStringLiteral("\\A(?:") +
280  QStringLiteral("([-+]?\\d{1,3}%1?\\d*)(?:%3)?(?:\\s*)%2(?:,|;|\\s)\\s*") +
281  QStringLiteral("([-+]?\\d{1,3}%1?\\d*)(?:%3)?(?:\\s*)%2") +
282  QStringLiteral(")\\z");
283 
284  // direction as prefix, e.g. N 74.2245 W 32.2434
285  const QString prefixCapExp = QStringLiteral("\\A(?:") +
286  QStringLiteral("%2\\s*([-+]?\\d{1,3}%1?\\d*)(?:%3)?\\s*(?:,|;|\\s)\\s*") +
287  QStringLiteral("%2\\s*([-+]?\\d{1,3}%1?\\d*)(?:%3)?") +
288  QStringLiteral(")\\z");
289 
290  const QString& expTemplate = (dirPosition == PostfixDir) ? postfixCapExp
291  : prefixCapExp;
292 
293  const QString numberCapExp = expTemplate.arg(m_decimalPointExp, m_dirCapExp, m_degreeExp);
294  const QRegularExpression regex(numberCapExp);
295 // qWarning() << regex.isValid() << regex.errorString() << regex.pattern();
296  QRegularExpressionMatch match = regex.match(input);
297  if (!match.hasMatch()) {
298 // qWarning() << "LonLatParser::tryMatchFromD -> no match";
299  return false;
300  }
301 // qWarning() << "LonLatParser::tryMatchFromD -> match" << match;
302 
303  bool isDir1LonDir;
304  bool isLonDirPosHemisphere;
305  bool isLatDirPosHemisphere;
306  const QString dir1 = match.captured(dirPosition == PostfixDir ? 2 : 1);
307  const QString dir2 = match.captured(dirPosition == PostfixDir ? 4 : 3);
308  if (!isCorrectDirections(dir1, dir2, isDir1LonDir,
309  isLonDirPosHemisphere, isLatDirPosHemisphere)) {
310  return false;
311  }
312 
313  const int valueStartIndex1 = (dirPosition == PostfixDir ? 1 : 2);
314  const int valueStartIndex2 = (dirPosition == PostfixDir ? 3 : 4);
315  m_lon = degreeValueFromD(match, isDir1LonDir ? valueStartIndex1 : valueStartIndex2,
316  isLonDirPosHemisphere);
317  m_lat = degreeValueFromD(match, isDir1LonDir ? valueStartIndex2 : valueStartIndex1,
318  isLatDirPosHemisphere);
319 
320  return true;
321 }
322 
323 double LonLatParser::parseDouble(const QString& input)
324 {
325  // Decide by decimalpoint if system locale or C locale should be tried.
326  // Otherwise if first trying with a system locale when the string is in C locale,
327  // the "." might be misinterpreted as thousands group separator and thus a wrong
328  // value yielded
330  return input.contains(locale.decimalPoint()) ? locale.toDouble(input) : input.toDouble();
331 }
332 
333 QString LonLatParser::createDecimalPointExp()
334 {
335  const QChar decimalPoint = QLocale::system().decimalPoint();
336 
337  return (decimalPoint == QLatin1Char('.')) ? QStringLiteral("\\.") :
338  QLatin1String("[.") + decimalPoint + QLatin1Char(']');
339 }
340 
341 void LonLatParser::getLocaleList(QStringList& localeList, const QString& localeListString,
342  const QLatin1String& placeholder, const QString& separator)
343 {
344  const QString lowerLocaleListString = localeListString.toLower();
345  if (lowerLocaleListString != placeholder) {
346  localeList = lowerLocaleListString.split(separator, QString::SkipEmptyParts);
347  }
348 }
349 
350 bool LonLatParser::isDirection(const QString& input, const QStringList& directions)
351 {
352  return (directions.contains(input));
353 }
354 
355 bool LonLatParser::isDirection(const QString& input, const QString& direction)
356 {
357  return (input == direction);
358 }
359 
360 bool LonLatParser::isOneOfDirections(const QString& input,
361  const QString& firstDirection,
362  const QString& secondDirection,
363  bool& isFirstDirection)
364 {
365  isFirstDirection = isDirection(input, firstDirection);
366  return isFirstDirection || isDirection(input, secondDirection);
367 }
368 
369 bool LonLatParser::isOneOfDirections(const QString& input,
370  const QStringList& firstDirections,
371  const QStringList& secondDirections,
372  bool& isFirstDirection)
373 {
374  isFirstDirection = isDirection(input, firstDirections);
375  return isFirstDirection || isDirection(input, secondDirections);
376 }
377 
378 
379 bool LonLatParser::isLocaleLonDirection(const QString& input,
380  bool& isDirPosHemisphere) const
381 {
382  return isOneOfDirections(input, m_eastLocale, m_westLocale, isDirPosHemisphere);
383 }
384 
385 bool LonLatParser::isLocaleLatDirection(const QString& input,
386  bool& isDirPosHemisphere) const
387 {
388  return isOneOfDirections(input, m_northLocale, m_southLocale, isDirPosHemisphere);
389 }
390 
391 bool LonLatParser::isLonDirection(const QString& input,
392  bool& isDirPosHemisphere) const
393 {
394  return isOneOfDirections(input, m_east, m_west, isDirPosHemisphere);
395 }
396 
397 bool LonLatParser::isLatDirection(const QString& input,
398  bool& isDirPosHemisphere) const
399 {
400  return isOneOfDirections(input, m_north, m_south, isDirPosHemisphere);
401 }
402 
403 
404 qreal LonLatParser::degreeValueFromDMS(const QRegularExpressionMatch& regexMatch, int c, bool isPosHemisphere)
405 {
406  const bool isNegativeValue = (regexMatch.captured(c++) == QLatin1String("-"));
407  const uint degree = regexMatch.captured(c++).toUInt();
408  const uint minutes = regexMatch.captured(c++).toUInt();
409  const qreal seconds = parseDouble(regexMatch.captured(c));
410 
411  qreal result = degree + (minutes * MIN2HOUR) + (seconds * SEC2HOUR);
412 
413  if (isNegativeValue) {
414  result *= -1;
415  }
416  if (! isPosHemisphere) {
417  result *= -1;
418  }
419 
420  return result;
421 }
422 
423 qreal LonLatParser::degreeValueFromDM(const QRegularExpressionMatch& regexMatch, int c, bool isPosHemisphere)
424 {
425  const bool isNegativeValue = (regexMatch.captured(c++) == QLatin1String("-"));
426  const uint degree = regexMatch.captured(c++).toUInt();
427  const qreal minutes = parseDouble(regexMatch.captured(c));
428 
429  qreal result = degree + (minutes * MIN2HOUR);
430 
431  if (isNegativeValue) {
432  result *= -1;
433  }
434  if (! isPosHemisphere) {
435  result *= -1;
436  }
437 
438  return result;
439 }
440 
441 qreal LonLatParser::degreeValueFromD(const QRegularExpressionMatch& regexMatch, int c, bool isPosHemisphere)
442 {
443  qreal result = parseDouble(regexMatch.captured(c));
444 
445  if (! isPosHemisphere) {
446  result *= -1;
447  }
448 
449  return result;
450 }
451 
452 bool LonLatParser::isCorrectDirections(const QString& dir1, const QString& dir2,
453  bool& isDir1LonDir,
454  bool& isLonDirPosHemisphere,
455  bool& isLatDirPosHemisphere) const
456 {
457  // first try localized names
458  isDir1LonDir = isLocaleLonDirection(dir1, isLonDirPosHemisphere);
459  const bool resultLocale = isDir1LonDir ?
460  isLocaleLatDirection(dir2, isLatDirPosHemisphere) :
461  (isLocaleLatDirection(dir1, isLatDirPosHemisphere) &&
462  isLocaleLonDirection(dir2, isLonDirPosHemisphere));
463 
464  if (resultLocale) {
465  return resultLocale;
466  }
467 
468  // fallback to try english names as lingua franca
469  isDir1LonDir = isLonDirection(dir1, isLonDirPosHemisphere);
470  return isDir1LonDir ?
471  isLatDirection(dir2, isLatDirPosHemisphere) :
472  (isLatDirection(dir1, isLatDirPosHemisphere) &&
473  isLonDirection(dir2, isLonDirPosHemisphere));
474 }
475 
476 }
QChar decimalPoint() const const
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool contains(const QString &str, Qt::CaseSensitivity cs) const const
QString trimmed() const const
QString escape(const QString &str)
QLocale system()
int length() const const
Binds a QML item to a specific geodetic location in screen coordinates.
uint toUInt(bool *ok, int base) const const
double toDouble(bool *ok) const const
LocaleWrapper locale()
QString toLower() const const
KIOFILEWIDGETS_EXPORT QString dir(const QString &fileClass)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
KStandardDirs * dirs()
const QChar at(int position) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString captured(int nth) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Thu Sep 21 2023 04:12:27 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.