1 /****************************************************************************
2 **
3 ** Copyright (C) 2017 Mapbox, Inc.
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 "qplacemanagerenginemapbox.h"
41 #include "qplacesearchreplymapbox.h"
42 #include "qplacesearchsuggestionreplymapbox.h"
43 #include "qplacecategoriesreplymapbox.h"
44 #include "qmapboxcommon.h"
45 
46 #include <QtCore/QUrlQuery>
47 #include <QtCore/QXmlStreamReader>
48 #include <QtCore/QRegularExpression>
49 #include <QtNetwork/QNetworkAccessManager>
50 #include <QtNetwork/QNetworkRequest>
51 #include <QtNetwork/QNetworkReply>
52 #include <QtPositioning/QGeoCircle>
53 #include <QtLocation/private/unsupportedreplies_p.h>
54 
55 #include <QtCore/QElapsedTimer>
56 
57 namespace {
58 
59 // https://www.mapbox.com/api-documentation/#poi-categories
60 static const QStringList categories = QStringList()
61     << QStringLiteral("bakery")
62     << QStringLiteral("bank")
63     << QStringLiteral("bar")
64     << QStringLiteral("cafe")
65     << QStringLiteral("church")
66     << QStringLiteral("cinema")
67     << QStringLiteral("coffee")
68     << QStringLiteral("concert")
69     << QStringLiteral("fast food")
70     << QStringLiteral("finance")
71     << QStringLiteral("gallery")
72     << QStringLiteral("historic")
73     << QStringLiteral("hotel")
74     << QStringLiteral("landmark")
75     << QStringLiteral("museum")
76     << QStringLiteral("music")
77     << QStringLiteral("park")
78     << QStringLiteral("pizza")
79     << QStringLiteral("restaurant")
80     << QStringLiteral("retail")
81     << QStringLiteral("school")
82     << QStringLiteral("shop")
83     << QStringLiteral("tea")
84     << QStringLiteral("theater")
85     << QStringLiteral("university");
86 
87 } // namespace
88 
89 // Mapbox API does not provide support for paginated place queries. This
90 // implementation is a wrapper around its Geocoding service:
91 // https://www.mapbox.com/api-documentation/#geocoding
QPlaceManagerEngineMapbox(const QVariantMap & parameters,QGeoServiceProvider::Error * error,QString * errorString)92 QPlaceManagerEngineMapbox::QPlaceManagerEngineMapbox(const QVariantMap &parameters, QGeoServiceProvider::Error *error, QString *errorString)
93     : QPlaceManagerEngine(parameters), m_networkManager(new QNetworkAccessManager(this))
94 {
95     if (parameters.contains(QStringLiteral("mapbox.useragent")))
96         m_userAgent = parameters.value(QStringLiteral("mapbox.useragent")).toString().toLatin1();
97     else
98         m_userAgent = mapboxDefaultUserAgent;
99 
100     m_accessToken = parameters.value(QStringLiteral("mapbox.access_token")).toString();
101 
102     m_isEnterprise = parameters.value(QStringLiteral("mapbox.enterprise")).toBool();
103     m_urlPrefix = m_isEnterprise ? mapboxGeocodingEnterpriseApiPath : mapboxGeocodingApiPath;
104 
105     *error = QGeoServiceProvider::NoError;
106     errorString->clear();
107 }
108 
~QPlaceManagerEngineMapbox()109 QPlaceManagerEngineMapbox::~QPlaceManagerEngineMapbox()
110 {
111 }
112 
search(const QPlaceSearchRequest & request)113 QPlaceSearchReply *QPlaceManagerEngineMapbox::search(const QPlaceSearchRequest &request)
114 {
115     return qobject_cast<QPlaceSearchReply *>(doSearch(request, PlaceSearchType::CompleteSearch));
116 }
117 
searchSuggestions(const QPlaceSearchRequest & request)118 QPlaceSearchSuggestionReply *QPlaceManagerEngineMapbox::searchSuggestions(const QPlaceSearchRequest &request)
119 {
120     return qobject_cast<QPlaceSearchSuggestionReply *>(doSearch(request, PlaceSearchType::SuggestionSearch));
121 }
122 
doSearch(const QPlaceSearchRequest & request,PlaceSearchType searchType)123 QPlaceReply *QPlaceManagerEngineMapbox::doSearch(const QPlaceSearchRequest &request, PlaceSearchType searchType)
124 {
125     const QGeoShape searchArea = request.searchArea();
126     const QString searchTerm = request.searchTerm();
127     const QString recommendationId = request.recommendationId();
128     const QList<QPlaceCategory> placeCategories = request.categories();
129 
130     bool invalidRequest = false;
131 
132     // QLocation::DeviceVisibility is not allowed for non-enterprise accounts.
133     if (!m_isEnterprise)
134         invalidRequest |= request.visibilityScope().testFlag(QLocation::DeviceVisibility);
135 
136     // Must provide either a search term, categories or recommendation.
137     invalidRequest |= searchTerm.isEmpty() && placeCategories.isEmpty() && recommendationId.isEmpty();
138 
139     // Category search must not provide recommendation, and vice-versa.
140     invalidRequest |= searchTerm.isEmpty() && !placeCategories.isEmpty() && !recommendationId.isEmpty();
141 
142     if (invalidRequest) {
143         QPlaceReply *reply;
144         if (searchType == PlaceSearchType::CompleteSearch)
145             reply = new QPlaceSearchReplyMapbox(request, 0, this);
146         else
147             reply = new QPlaceSearchSuggestionReplyMapbox(0, this);
148 
149         connect(reply, &QPlaceReply::finished, this, &QPlaceManagerEngineMapbox::onReplyFinished);
150         connect(reply, QOverload<QPlaceReply::Error, const QString &>::of(&QPlaceReply::error),
151                 this, &QPlaceManagerEngineMapbox::onReplyError);
152 
153         QMetaObject::invokeMethod(reply, "setError", Qt::QueuedConnection,
154                 Q_ARG(QPlaceReply::Error, QPlaceReply::BadArgumentError),
155                 Q_ARG(QString, "Invalid request."));
156 
157         return reply;
158     }
159 
160     QString queryString;
161     if (!searchTerm.isEmpty()) {
162         queryString = searchTerm;
163     } else if (!recommendationId.isEmpty()) {
164         queryString = recommendationId;
165     } else {
166         QStringList similarIds;
167         for (const QPlaceCategory &placeCategory : placeCategories)
168             similarIds.append(placeCategory.categoryId());
169         queryString = similarIds.join(QLatin1Char(','));
170     }
171     queryString.append(QStringLiteral(".json"));
172 
173     // https://www.mapbox.com/api-documentation/#request-format
174     QUrl requestUrl(m_urlPrefix + queryString);
175 
176     QUrlQuery queryItems;
177     queryItems.addQueryItem(QStringLiteral("access_token"), m_accessToken);
178 
179     // XXX: Investigate situations where we need to filter by 'country'.
180 
181     QStringList languageCodes;
182     for (const QLocale& locale: qAsConst(m_locales)) {
183         // Returns the language and country of this locale as a string of the
184         // form "language_country", where language is a lowercase, two-letter
185         // ISO 639 language code, and country is an uppercase, two- or
186         // three-letter ISO 3166 country code.
187 
188         if (locale.language() == QLocale::C)
189             continue;
190 
191         const QString languageCode = locale.name().section(QLatin1Char('_'), 0, 0);
192         if (!languageCodes.contains(languageCode))
193             languageCodes.append(languageCode);
194     }
195 
196     if (!languageCodes.isEmpty())
197         queryItems.addQueryItem(QStringLiteral("language"), languageCodes.join(QLatin1Char(',')));
198 
199     if (searchArea.type() != QGeoShape::UnknownType) {
200         const QGeoCoordinate center = searchArea.center();
201         queryItems.addQueryItem(QStringLiteral("proximity"),
202                                 QString::number(center.longitude()) + QLatin1Char(',') + QString::number(center.latitude()));
203     }
204 
205     queryItems.addQueryItem(QStringLiteral("type"), QStringLiteral("poi"));
206 
207     // XXX: Investigate situations where 'autocomplete' should be disabled.
208 
209     QGeoRectangle boundingBox = searchArea.boundingGeoRectangle();
210     if (!boundingBox.isEmpty()) {
211         queryItems.addQueryItem(QStringLiteral("bbox"),
212                 QString::number(boundingBox.topLeft().longitude()) + QLatin1Char(',') +
213                 QString::number(boundingBox.bottomRight().latitude()) + QLatin1Char(',') +
214                 QString::number(boundingBox.bottomRight().longitude()) + QLatin1Char(',') +
215                 QString::number(boundingBox.topLeft().latitude()));
216     }
217 
218     if (request.limit() > 0)
219         queryItems.addQueryItem(QStringLiteral("limit"), QString::number(request.limit()));
220 
221     // XXX: Investigate searchContext() use cases.
222 
223     requestUrl.setQuery(queryItems);
224 
225     QNetworkRequest networkRequest(requestUrl);
226     networkRequest.setHeader(QNetworkRequest::UserAgentHeader, m_userAgent);
227 
228     QNetworkReply *networkReply = m_networkManager->get(networkRequest);
229     QPlaceReply *reply;
230     if (searchType == PlaceSearchType::CompleteSearch)
231         reply = new QPlaceSearchReplyMapbox(request, networkReply, this);
232     else
233         reply = new QPlaceSearchSuggestionReplyMapbox(networkReply, this);
234 
235     connect(reply, &QPlaceReply::finished, this, &QPlaceManagerEngineMapbox::onReplyFinished);
236     connect(reply, QOverload<QPlaceReply::Error, const QString &>::of(&QPlaceReply::error),
237             this, &QPlaceManagerEngineMapbox::onReplyError);
238 
239     return reply;
240 }
241 
initializeCategories()242 QPlaceReply *QPlaceManagerEngineMapbox::initializeCategories()
243 {
244     if (m_categories.isEmpty()) {
245         for (const QString &categoryId : categories) {
246             QPlaceCategory category;
247             category.setName(QMapboxCommon::mapboxNameForCategory(categoryId));
248             category.setCategoryId(categoryId);
249             category.setVisibility(QLocation::PublicVisibility);
250             m_categories[categoryId] = category;
251         }
252     }
253 
254     QPlaceCategoriesReplyMapbox *reply = new QPlaceCategoriesReplyMapbox(this);
255     connect(reply, &QPlaceReply::finished, this, &QPlaceManagerEngineMapbox::onReplyFinished);
256     connect(reply, QOverload<QPlaceReply::Error, const QString &>::of(&QPlaceReply::error),
257             this, &QPlaceManagerEngineMapbox::onReplyError);
258 
259     // Queue a future finished() emission from the reply.
260     QMetaObject::invokeMethod(reply, "finish", Qt::QueuedConnection);
261 
262     return reply;
263 }
264 
parentCategoryId(const QString & categoryId) const265 QString QPlaceManagerEngineMapbox::parentCategoryId(const QString &categoryId) const
266 {
267     Q_UNUSED(categoryId);
268 
269     // Only a single category level.
270     return QString();
271 }
272 
childCategoryIds(const QString & categoryId) const273 QStringList QPlaceManagerEngineMapbox::childCategoryIds(const QString &categoryId) const
274 {
275     // Only a single category level.
276     if (categoryId.isEmpty())
277         return m_categories.keys();
278 
279     return QStringList();
280 }
281 
category(const QString & categoryId) const282 QPlaceCategory QPlaceManagerEngineMapbox::category(const QString &categoryId) const
283 {
284     return m_categories.value(categoryId);
285 }
286 
childCategories(const QString & parentId) const287 QList<QPlaceCategory> QPlaceManagerEngineMapbox::childCategories(const QString &parentId) const
288 {
289     // Only a single category level.
290     if (parentId.isEmpty())
291         return m_categories.values();
292 
293     return QList<QPlaceCategory>();
294 }
295 
locales() const296 QList<QLocale> QPlaceManagerEngineMapbox::locales() const
297 {
298     return m_locales;
299 }
300 
setLocales(const QList<QLocale> & locales)301 void QPlaceManagerEngineMapbox::setLocales(const QList<QLocale> &locales)
302 {
303     m_locales = locales;
304 }
305 
onReplyFinished()306 void QPlaceManagerEngineMapbox::onReplyFinished()
307 {
308     QPlaceReply *reply = qobject_cast<QPlaceReply *>(sender());
309     if (reply)
310         emit finished(reply);
311 }
312 
onReplyError(QPlaceReply::Error errorCode,const QString & errorString)313 void QPlaceManagerEngineMapbox::onReplyError(QPlaceReply::Error errorCode, const QString &errorString)
314 {
315     QPlaceReply *reply = qobject_cast<QPlaceReply *>(sender());
316     if (reply)
317         emit error(reply, errorCode, errorString);
318 }
319