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

KDE's Doxygen guidelines are available online.