Marble

LonLatParser.cpp
1// SPDX-License-Identifier: LGPL-2.1-or-later
2//
3// SPDX-FileCopyrightText: 2004-2007 Torsten Rahn <tackat@kde.org>
4// SPDX-FileCopyrightText: 2007-2008 Inge Wallin <ingwa@kde.org>
5// SPDX-FileCopyrightText: 2008 Patrick Spendrin <ps_ml@gmx.de>
6// SPDX-FileCopyrightText: 2011 Friedrich W. H. Kossebau <kossebau@kde.org>
7// SPDX-FileCopyrightText: 2011 Bernhard Beschow <bbeschow@cs.tu-berlin.de>
8// SPDX-FileCopyrightText: 2015 Alejandro Garcia Montoro <alejandro.garciamontoro@gmail.com>
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
22namespace Marble
23{
24
25LonLatParser::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
37void 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
128bool 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
179bool 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
230bool 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
276bool 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
323double 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
329 QLocale locale = QLocale::system();
330 return input.contains(locale.decimalPoint()) ? locale.toDouble(input) : input.toDouble();
331}
332
333QString LonLatParser::createDecimalPointExp()
334{
335 const QChar decimalPoint = QLocale::system().decimalPoint();
336
337 return (decimalPoint == QLatin1Char('.')) ? QStringLiteral("\\.") :
338 QLatin1String("[.") + decimalPoint + QLatin1Char(']');
339}
340
341void 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
350bool LonLatParser::isDirection(const QString& input, const QStringList& directions)
351{
352 return (directions.contains(input));
353}
354
355bool LonLatParser::isDirection(const QString& input, const QString& direction)
356{
357 return (input == direction);
358}
359
360bool 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
369bool 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
379bool LonLatParser::isLocaleLonDirection(const QString& input,
380 bool& isDirPosHemisphere) const
381{
382 return isOneOfDirections(input, m_eastLocale, m_westLocale, isDirPosHemisphere);
383}
384
385bool LonLatParser::isLocaleLatDirection(const QString& input,
386 bool& isDirPosHemisphere) const
387{
388 return isOneOfDirections(input, m_northLocale, m_southLocale, isDirPosHemisphere);
389}
390
391bool LonLatParser::isLonDirection(const QString& input,
392 bool& isDirPosHemisphere) const
393{
394 return isOneOfDirections(input, m_east, m_west, isDirPosHemisphere);
395}
396
397bool LonLatParser::isLatDirection(const QString& input,
398 bool& isDirPosHemisphere) const
399{
400 return isOneOfDirections(input, m_north, m_south, isDirPosHemisphere);
401}
402
403
404qreal 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
423qreal 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
441qreal 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
452bool 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}
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
KIOCORE_EXPORT QString dir(const QString &fileClass)
Binds a QML item to a specific geodetic location in screen coordinates.
QString decimalPoint() const const
QLocale system()
double toDouble(QStringView s, bool *ok) const const
QString escape(QStringView str)
QString captured(QStringView name) const const
QString arg(Args &&... args) const const
const QChar at(qsizetype position) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
qsizetype length() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
double toDouble(bool *ok) const const
QString toLower() const const
uint toUInt(bool *ok, int base) const const
QString trimmed() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 26 2024 11:57:57 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.