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 ¶meters, 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