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 &parameters,
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