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
57namespace {
58
59// https://www.mapbox.com/api-documentation/#poi-categories
60static 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
92QPlaceManagerEngineMapbox::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
109QPlaceManagerEngineMapbox::~QPlaceManagerEngineMapbox()
110{
111}
112
113QPlaceSearchReply *QPlaceManagerEngineMapbox::search(const QPlaceSearchRequest &request)
114{
115 return qobject_cast<QPlaceSearchReply *>(object: doSearch(request, PlaceSearchType::CompleteSearch));
116}
117
118QPlaceSearchSuggestionReply *QPlaceManagerEngineMapbox::searchSuggestions(const QPlaceSearchRequest &request)
119{
120 return qobject_cast<QPlaceSearchSuggestionReply *>(object: doSearch(request, PlaceSearchType::SuggestionSearch));
121}
122
123QPlaceReply *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(flag: 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(sender: reply, signal: &QPlaceReply::finished, receiver: this, slot: &QPlaceManagerEngineMapbox::onReplyFinished);
150 connect(sender: reply, signal: QOverload<QPlaceReply::Error, const QString &>::of(ptr: &QPlaceReply::error),
151 receiver: this, slot: &QPlaceManagerEngineMapbox::onReplyError);
152
153 QMetaObject::invokeMethod(obj: reply, member: "setError", type: 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(t: placeCategory.categoryId());
169 queryString = similarIds.join(sep: 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"), value: m_accessToken);
178
179 // XXX: Investigate situations where we need to filter by 'country'.
180
181 QStringList languageCodes;
182 for (const QLocale& locale: qAsConst(t&: 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(asep: QLatin1Char('_'), astart: 0, aend: 0);
192 if (!languageCodes.contains(str: languageCode))
193 languageCodes.append(t: languageCode);
194 }
195
196 if (!languageCodes.isEmpty())
197 queryItems.addQueryItem(QStringLiteral("language"), value: languageCodes.join(sep: QLatin1Char(',')));
198
199 if (searchArea.type() != QGeoShape::UnknownType) {
200 const QGeoCoordinate center = searchArea.center();
201 queryItems.addQueryItem(QStringLiteral("proximity"),
202 value: 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 value: 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"), value: QString::number(request.limit()));
220
221 // XXX: Investigate searchContext() use cases.
222
223 requestUrl.setQuery(queryItems);
224
225 QNetworkRequest networkRequest(requestUrl);
226 networkRequest.setHeader(header: QNetworkRequest::UserAgentHeader, value: m_userAgent);
227
228 QNetworkReply *networkReply = m_networkManager->get(request: 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(sender: reply, signal: &QPlaceReply::finished, receiver: this, slot: &QPlaceManagerEngineMapbox::onReplyFinished);
236 connect(sender: reply, signal: QOverload<QPlaceReply::Error, const QString &>::of(ptr: &QPlaceReply::error),
237 receiver: this, slot: &QPlaceManagerEngineMapbox::onReplyError);
238
239 return reply;
240}
241
242QPlaceReply *QPlaceManagerEngineMapbox::initializeCategories()
243{
244 if (m_categories.isEmpty()) {
245 for (const QString &categoryId : categories) {
246 QPlaceCategory category;
247 category.setName(QMapboxCommon::mapboxNameForCategory(category: 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(sender: reply, signal: &QPlaceReply::finished, receiver: this, slot: &QPlaceManagerEngineMapbox::onReplyFinished);
256 connect(sender: reply, signal: QOverload<QPlaceReply::Error, const QString &>::of(ptr: &QPlaceReply::error),
257 receiver: this, slot: &QPlaceManagerEngineMapbox::onReplyError);
258
259 // Queue a future finished() emission from the reply.
260 QMetaObject::invokeMethod(obj: reply, member: "finish", type: Qt::QueuedConnection);
261
262 return reply;
263}
264
265QString QPlaceManagerEngineMapbox::parentCategoryId(const QString &categoryId) const
266{
267 Q_UNUSED(categoryId);
268
269 // Only a single category level.
270 return QString();
271}
272
273QStringList 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
282QPlaceCategory QPlaceManagerEngineMapbox::category(const QString &categoryId) const
283{
284 return m_categories.value(akey: categoryId);
285}
286
287QList<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
296QList<QLocale> QPlaceManagerEngineMapbox::locales() const
297{
298 return m_locales;
299}
300
301void QPlaceManagerEngineMapbox::setLocales(const QList<QLocale> &locales)
302{
303 m_locales = locales;
304}
305
306void QPlaceManagerEngineMapbox::onReplyFinished()
307{
308 QPlaceReply *reply = qobject_cast<QPlaceReply *>(object: sender());
309 if (reply)
310 emit finished(reply);
311}
312
313void QPlaceManagerEngineMapbox::onReplyError(QPlaceReply::Error errorCode, const QString &errorString)
314{
315 QPlaceReply *reply = qobject_cast<QPlaceReply *>(object: sender());
316 if (reply)
317 emit error(reply, error: errorCode, errorString);
318}
319

source code of qtlocation/src/plugins/geoservices/mapbox/qplacemanagerenginemapbox.cpp