1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 Aaron McCarthy <mccarthy.aaron@gmail.com>
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the QtFoo module of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or (at your option) the GNU General
28 ** Public license version 3 or any later version approved by the KDE Free
29 ** Qt Foundation. The licenses are as published by the Free Software
30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31 ** included in the packaging of this file. Please review the following
32 ** information to ensure the GNU General Public License requirements will
33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34 ** https://www.gnu.org/licenses/gpl-3.0.html.
35 **
36 ** $QT_END_LICENSE$
37 **
38 ****************************************************************************/
39
40 #include "qplacemanagerengineosm.h"
41 #include "qplacesearchreplyosm.h"
42 #include "qplacecategoriesreplyosm.h"
43
44 #include <QtCore/QUrlQuery>
45 #include <QtCore/QXmlStreamReader>
46 #include <QtCore/QRegularExpression>
47 #include <QtNetwork/QNetworkAccessManager>
48 #include <QtNetwork/QNetworkRequest>
49 #include <QtNetwork/QNetworkReply>
50 #include <QtPositioning/QGeoCircle>
51 #include <QtLocation/private/unsupportedreplies_p.h>
52
53 #include <QtCore/QElapsedTimer>
54
55 namespace
56 {
57 QString SpecialPhrasesBaseUrl = QStringLiteral("http://wiki.openstreetmap.org/wiki/Special:Export/Nominatim/Special_Phrases/");
58
nameForTagKey(const QString & tagKey)59 QString nameForTagKey(const QString &tagKey)
60 {
61 if (tagKey == QLatin1String("aeroway"))
62 return QPlaceManagerEngineOsm::tr("Aeroway");
63 else if (tagKey == QLatin1String("amenity"))
64 return QPlaceManagerEngineOsm::tr("Amenity");
65 else if (tagKey == QLatin1String("building"))
66 return QPlaceManagerEngineOsm::tr("Building");
67 else if (tagKey == QLatin1String("highway"))
68 return QPlaceManagerEngineOsm::tr("Highway");
69 else if (tagKey == QLatin1String("historic"))
70 return QPlaceManagerEngineOsm::tr("Historic");
71 else if (tagKey == QLatin1String("landuse"))
72 return QPlaceManagerEngineOsm::tr("Land use");
73 else if (tagKey == QLatin1String("leisure"))
74 return QPlaceManagerEngineOsm::tr("Leisure");
75 else if (tagKey == QLatin1String("man_made"))
76 return QPlaceManagerEngineOsm::tr("Man made");
77 else if (tagKey == QLatin1String("natural"))
78 return QPlaceManagerEngineOsm::tr("Natural");
79 else if (tagKey == QLatin1String("place"))
80 return QPlaceManagerEngineOsm::tr("Place");
81 else if (tagKey == QLatin1String("railway"))
82 return QPlaceManagerEngineOsm::tr("Railway");
83 else if (tagKey == QLatin1String("shop"))
84 return QPlaceManagerEngineOsm::tr("Shop");
85 else if (tagKey == QLatin1String("tourism"))
86 return QPlaceManagerEngineOsm::tr("Tourism");
87 else if (tagKey == QLatin1String("waterway"))
88 return QPlaceManagerEngineOsm::tr("Waterway");
89 else
90 return tagKey;
91 }
92
93 }
94
QPlaceManagerEngineOsm(const QVariantMap & parameters,QGeoServiceProvider::Error * error,QString * errorString)95 QPlaceManagerEngineOsm::QPlaceManagerEngineOsm(const QVariantMap ¶meters,
96 QGeoServiceProvider::Error *error,
97 QString *errorString)
98 : QPlaceManagerEngine(parameters), m_networkManager(new QNetworkAccessManager(this)),
99 m_categoriesReply(0)
100 {
101 if (parameters.contains(QStringLiteral("osm.useragent")))
102 m_userAgent = parameters.value(QStringLiteral("osm.useragent")).toString().toLatin1();
103 else
104 m_userAgent = "Qt Location based application";
105
106 if (parameters.contains(QStringLiteral("osm.places.host")))
107 m_urlPrefix = parameters.value(QStringLiteral("osm.places.host")).toString();
108 else
109 m_urlPrefix = QStringLiteral("http://nominatim.openstreetmap.org/search");
110
111
112 if (parameters.contains(QStringLiteral("osm.places.debug_query")))
113 m_debugQuery = parameters.value(QStringLiteral("osm.places.debug_query")).toBool();
114
115 if (parameters.contains(QStringLiteral("osm.places.page_size"))
116 && parameters.value(QStringLiteral("osm.places.page_size")).canConvert<int>())
117 m_pageSize = parameters.value(QStringLiteral("osm.places.page_size")).toInt();
118
119 *error = QGeoServiceProvider::NoError;
120 errorString->clear();
121 }
122
~QPlaceManagerEngineOsm()123 QPlaceManagerEngineOsm::~QPlaceManagerEngineOsm()
124 {
125 }
126
search(const QPlaceSearchRequest & request)127 QPlaceSearchReply *QPlaceManagerEngineOsm::search(const QPlaceSearchRequest &request)
128 {
129 bool unsupported = false;
130
131 // Only public visibility supported
132 unsupported |= request.visibilityScope() != QLocation::UnspecifiedVisibility &&
133 request.visibilityScope() != QLocation::PublicVisibility;
134 unsupported |= request.searchTerm().isEmpty() && request.categories().isEmpty();
135
136 if (unsupported)
137 return QPlaceManagerEngine::search(request);
138
139 QUrlQuery queryItems;
140
141 queryItems.addQueryItem(QStringLiteral("format"), QStringLiteral("jsonv2"));
142
143 //queryItems.addQueryItem(QStringLiteral("accept-language"), QStringLiteral("en"));
144
145 QGeoRectangle boundingBox = request.searchArea().boundingGeoRectangle();
146
147 if (!boundingBox.isEmpty()) {
148 queryItems.addQueryItem(QStringLiteral("bounded"), QStringLiteral("1"));
149 QString coordinates;
150 coordinates = QString::number(boundingBox.topLeft().longitude()) + QLatin1Char(',') +
151 QString::number(boundingBox.topLeft().latitude()) + QLatin1Char(',') +
152 QString::number(boundingBox.bottomRight().longitude()) + QLatin1Char(',') +
153 QString::number(boundingBox.bottomRight().latitude());
154 queryItems.addQueryItem(QStringLiteral("viewbox"), coordinates);
155 }
156
157 QStringList queryParts;
158 if (!request.searchTerm().isEmpty())
159 queryParts.append(request.searchTerm());
160
161 foreach (const QPlaceCategory &category, request.categories()) {
162 QString id = category.categoryId();
163 int index = id.indexOf(QLatin1Char('='));
164 if (index != -1)
165 id = id.mid(index+1);
166 queryParts.append(QLatin1Char('[') + id + QLatin1Char(']'));
167 }
168
169 queryItems.addQueryItem(QStringLiteral("q"), queryParts.join(QLatin1Char('+')));
170
171 QVariantMap parameters = request.searchContext().toMap();
172
173 QStringList placeIds = parameters.value(QStringLiteral("ExcludePlaceIds")).toStringList();
174 if (!placeIds.isEmpty())
175 queryItems.addQueryItem(QStringLiteral("exclude_place_ids"), placeIds.join(QLatin1Char(',')));
176
177 queryItems.addQueryItem(QStringLiteral("addressdetails"), QStringLiteral("1"));
178 queryItems.addQueryItem(QStringLiteral("limit"), (request.limit() > 0) ? QString::number(request.limit())
179 : QString::number(m_pageSize));
180
181 QUrl requestUrl(m_urlPrefix);
182 requestUrl.setQuery(queryItems);
183
184 QNetworkRequest rq(requestUrl);
185 rq.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
186 QNetworkReply *networkReply = m_networkManager->get(rq);
187
188 QPlaceSearchReplyOsm *reply = new QPlaceSearchReplyOsm(request, networkReply, this);
189 connect(reply, SIGNAL(finished()), this, SLOT(replyFinished()));
190 connect(reply, SIGNAL(error(QPlaceReply::Error,QString)),
191 this, SLOT(replyError(QPlaceReply::Error,QString)));
192
193 if (m_debugQuery)
194 reply->requestUrl = requestUrl.url(QUrl::None);
195
196 return reply;
197 }
198
initializeCategories()199 QPlaceReply *QPlaceManagerEngineOsm::initializeCategories()
200 {
201 // Only fetch categories once
202 if (m_categories.isEmpty() && !m_categoriesReply) {
203 m_categoryLocales = m_locales;
204 m_categoryLocales.append(QLocale(QLocale::English));
205 fetchNextCategoryLocale();
206 }
207
208 QPlaceCategoriesReplyOsm *reply = new QPlaceCategoriesReplyOsm(this);
209 connect(reply, SIGNAL(finished()), this, SLOT(replyFinished()));
210 connect(reply, SIGNAL(error(QPlaceReply::Error,QString)),
211 this, SLOT(replyError(QPlaceReply::Error,QString)));
212
213 // TODO delayed finished() emission
214 if (!m_categories.isEmpty())
215 reply->emitFinished();
216
217 m_pendingCategoriesReply.append(reply);
218 return reply;
219 }
220
parentCategoryId(const QString & categoryId) const221 QString QPlaceManagerEngineOsm::parentCategoryId(const QString &categoryId) const
222 {
223 Q_UNUSED(categoryId);
224
225 // Only a two category levels
226 return QString();
227 }
228
childCategoryIds(const QString & categoryId) const229 QStringList QPlaceManagerEngineOsm::childCategoryIds(const QString &categoryId) const
230 {
231 return m_subcategories.value(categoryId);
232 }
233
category(const QString & categoryId) const234 QPlaceCategory QPlaceManagerEngineOsm::category(const QString &categoryId) const
235 {
236 return m_categories.value(categoryId);
237 }
238
childCategories(const QString & parentId) const239 QList<QPlaceCategory> QPlaceManagerEngineOsm::childCategories(const QString &parentId) const
240 {
241 QList<QPlaceCategory> categories;
242 foreach (const QString &id, m_subcategories.value(parentId))
243 categories.append(m_categories.value(id));
244 return categories;
245 }
246
locales() const247 QList<QLocale> QPlaceManagerEngineOsm::locales() const
248 {
249 return m_locales;
250 }
251
setLocales(const QList<QLocale> & locales)252 void QPlaceManagerEngineOsm::setLocales(const QList<QLocale> &locales)
253 {
254 m_locales = locales;
255 }
256
categoryReplyFinished()257 void QPlaceManagerEngineOsm::categoryReplyFinished()
258 {
259 QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
260 reply->deleteLater();
261
262 QXmlStreamReader parser(reply);
263 while (!parser.atEnd() && parser.readNextStartElement()) {
264 if (parser.name() == QLatin1String("mediawiki"))
265 continue;
266 if (parser.name() == QLatin1String("page"))
267 continue;
268 if (parser.name() == QLatin1String("revision"))
269 continue;
270 if (parser.name() == QLatin1String("text")) {
271 // parse
272 QString page = parser.readElementText();
273 QRegularExpression regex(QStringLiteral("\\| ([^|]+) \\|\\| ([^|]+) \\|\\| ([^|]+) \\|\\| ([^|]+) \\|\\| ([\\-YN])"));
274 QRegularExpressionMatchIterator i = regex.globalMatch(page);
275 while (i.hasNext()) {
276 QRegularExpressionMatch match = i.next();
277 QString name = match.capturedRef(1).toString();
278 QString tagKey = match.capturedRef(2).toString();
279 QString tagValue = match.capturedRef(3).toString();
280 QString op = match.capturedRef(4).toString();
281 QString plural = match.capturedRef(5).toString();
282
283 // Only interested in any operator plural forms
284 if (op != QLatin1String("-") || plural != QLatin1String("Y"))
285 continue;
286
287 if (!m_categories.contains(tagKey)) {
288 QPlaceCategory category;
289 category.setCategoryId(tagKey);
290 category.setName(nameForTagKey(tagKey));
291 m_categories.insert(category.categoryId(), category);
292 m_subcategories[QString()].append(tagKey);
293 emit categoryAdded(category, QString());
294 }
295
296 QPlaceCategory category;
297 category.setCategoryId(tagKey + QLatin1Char('=') + tagValue);
298 category.setName(name);
299
300 if (!m_categories.contains(category.categoryId())) {
301 m_categories.insert(category.categoryId(), category);
302 m_subcategories[tagKey].append(category.categoryId());
303 emit categoryAdded(category, tagKey);
304 }
305 }
306 }
307
308 parser.skipCurrentElement();
309 }
310
311 if (m_categories.isEmpty() && !m_categoryLocales.isEmpty()) {
312 fetchNextCategoryLocale();
313 return;
314 } else {
315 m_categoryLocales.clear();
316 }
317
318 foreach (QPlaceCategoriesReplyOsm *reply, m_pendingCategoriesReply)
319 reply->emitFinished();
320 m_pendingCategoriesReply.clear();
321 }
322
categoryReplyError()323 void QPlaceManagerEngineOsm::categoryReplyError()
324 {
325 foreach (QPlaceCategoriesReplyOsm *reply, m_pendingCategoriesReply)
326 reply->setError(QPlaceReply::CommunicationError, tr("Network request error"));
327 }
328
replyFinished()329 void QPlaceManagerEngineOsm::replyFinished()
330 {
331 QPlaceReply *reply = qobject_cast<QPlaceReply *>(sender());
332 if (reply)
333 emit finished(reply);
334 }
335
replyError(QPlaceReply::Error errorCode,const QString & errorString)336 void QPlaceManagerEngineOsm::replyError(QPlaceReply::Error errorCode, const QString &errorString)
337 {
338 QPlaceReply *reply = qobject_cast<QPlaceReply *>(sender());
339 if (reply)
340 emit error(reply, errorCode, errorString);
341 }
342
fetchNextCategoryLocale()343 void QPlaceManagerEngineOsm::fetchNextCategoryLocale()
344 {
345 if (m_categoryLocales.isEmpty()) {
346 qWarning("No locales specified to fetch categories for");
347 return;
348 }
349
350 QLocale locale = m_categoryLocales.takeFirst();
351
352 // FIXME: Categories should be cached.
353 QUrl requestUrl = QUrl(SpecialPhrasesBaseUrl + locale.name().left(2).toUpper());
354
355 m_categoriesReply = m_networkManager->get(QNetworkRequest(requestUrl));
356 connect(m_categoriesReply, SIGNAL(finished()), this, SLOT(categoryReplyFinished()));
357 connect(m_categoriesReply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)),
358 this, SLOT(categoryReplyError()));
359 }
360