/**************************************************************************** ** ** Copyright (C) 2017 Mapbox, Inc. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtFoo module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or (at your option) the GNU General ** Public license version 3 or any later version approved by the KDE Free ** Qt Foundation. The licenses are as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-2.0.html and ** https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qplacemanagerenginemapbox.h" #include "qplacesearchreplymapbox.h" #include "qplacesearchsuggestionreplymapbox.h" #include "qplacecategoriesreplymapbox.h" #include "qmapboxcommon.h" #include #include #include #include #include #include #include #include #include namespace { // https://www.mapbox.com/api-documentation/#poi-categories static const QStringList categories = QStringList() << QStringLiteral("bakery") << QStringLiteral("bank") << QStringLiteral("bar") << QStringLiteral("cafe") << QStringLiteral("church") << QStringLiteral("cinema") << QStringLiteral("coffee") << QStringLiteral("concert") << QStringLiteral("fast food") << QStringLiteral("finance") << QStringLiteral("gallery") << QStringLiteral("historic") << QStringLiteral("hotel") << QStringLiteral("landmark") << QStringLiteral("museum") << QStringLiteral("music") << QStringLiteral("park") << QStringLiteral("pizza") << QStringLiteral("restaurant") << QStringLiteral("retail") << QStringLiteral("school") << QStringLiteral("shop") << QStringLiteral("tea") << QStringLiteral("theater") << QStringLiteral("university"); } // namespace // Mapbox API does not provide support for paginated place queries. This // implementation is a wrapper around its Geocoding service: // https://www.mapbox.com/api-documentation/#geocoding QPlaceManagerEngineMapbox::QPlaceManagerEngineMapbox(const QVariantMap ¶meters, QGeoServiceProvider::Error *error, QString *errorString) : QPlaceManagerEngine(parameters), m_networkManager(new QNetworkAccessManager(this)) { if (parameters.contains(QStringLiteral("mapbox.useragent"))) m_userAgent = parameters.value(QStringLiteral("mapbox.useragent")).toString().toLatin1(); else m_userAgent = mapboxDefaultUserAgent; m_accessToken = parameters.value(QStringLiteral("mapbox.access_token")).toString(); m_isEnterprise = parameters.value(QStringLiteral("mapbox.enterprise")).toBool(); m_urlPrefix = m_isEnterprise ? mapboxGeocodingEnterpriseApiPath : mapboxGeocodingApiPath; *error = QGeoServiceProvider::NoError; errorString->clear(); } QPlaceManagerEngineMapbox::~QPlaceManagerEngineMapbox() { } QPlaceSearchReply *QPlaceManagerEngineMapbox::search(const QPlaceSearchRequest &request) { return qobject_cast(doSearch(request, PlaceSearchType::CompleteSearch)); } QPlaceSearchSuggestionReply *QPlaceManagerEngineMapbox::searchSuggestions(const QPlaceSearchRequest &request) { return qobject_cast(doSearch(request, PlaceSearchType::SuggestionSearch)); } QPlaceReply *QPlaceManagerEngineMapbox::doSearch(const QPlaceSearchRequest &request, PlaceSearchType searchType) { const QGeoShape searchArea = request.searchArea(); const QString searchTerm = request.searchTerm(); const QString recommendationId = request.recommendationId(); const QList placeCategories = request.categories(); bool invalidRequest = false; // QLocation::DeviceVisibility is not allowed for non-enterprise accounts. if (!m_isEnterprise) invalidRequest |= request.visibilityScope().testFlag(QLocation::DeviceVisibility); // Must provide either a search term, categories or recommendation. invalidRequest |= searchTerm.isEmpty() && placeCategories.isEmpty() && recommendationId.isEmpty(); // Category search must not provide recommendation, and vice-versa. invalidRequest |= searchTerm.isEmpty() && !placeCategories.isEmpty() && !recommendationId.isEmpty(); if (invalidRequest) { QPlaceReply *reply; if (searchType == PlaceSearchType::CompleteSearch) reply = new QPlaceSearchReplyMapbox(request, 0, this); else reply = new QPlaceSearchSuggestionReplyMapbox(0, this); connect(reply, &QPlaceReply::finished, this, &QPlaceManagerEngineMapbox::onReplyFinished); connect(reply, QOverload::of(&QPlaceReply::error), this, &QPlaceManagerEngineMapbox::onReplyError); QMetaObject::invokeMethod(reply, "setError", Qt::QueuedConnection, Q_ARG(QPlaceReply::Error, QPlaceReply::BadArgumentError), Q_ARG(QString, "Invalid request.")); return reply; } QString queryString; if (!searchTerm.isEmpty()) { queryString = searchTerm; } else if (!recommendationId.isEmpty()) { queryString = recommendationId; } else { QStringList similarIds; for (const QPlaceCategory &placeCategory : placeCategories) similarIds.append(placeCategory.categoryId()); queryString = similarIds.join(QLatin1Char(',')); } queryString.append(QStringLiteral(".json")); // https://www.mapbox.com/api-documentation/#request-format QUrl requestUrl(m_urlPrefix + queryString); QUrlQuery queryItems; queryItems.addQueryItem(QStringLiteral("access_token"), m_accessToken); // XXX: Investigate situations where we need to filter by 'country'. QStringList languageCodes; for (const QLocale& locale: qAsConst(m_locales)) { // Returns the language and country of this locale as a string of the // form "language_country", where language is a lowercase, two-letter // ISO 639 language code, and country is an uppercase, two- or // three-letter ISO 3166 country code. if (locale.language() == QLocale::C) continue; const QString languageCode = locale.name().section(QLatin1Char('_'), 0, 0); if (!languageCodes.contains(languageCode)) languageCodes.append(languageCode); } if (!languageCodes.isEmpty()) queryItems.addQueryItem(QStringLiteral("language"), languageCodes.join(QLatin1Char(','))); if (searchArea.type() != QGeoShape::UnknownType) { const QGeoCoordinate center = searchArea.center(); queryItems.addQueryItem(QStringLiteral("proximity"), QString::number(center.longitude()) + QLatin1Char(',') + QString::number(center.latitude())); } queryItems.addQueryItem(QStringLiteral("type"), QStringLiteral("poi")); // XXX: Investigate situations where 'autocomplete' should be disabled. QGeoRectangle boundingBox = searchArea.boundingGeoRectangle(); if (!boundingBox.isEmpty()) { queryItems.addQueryItem(QStringLiteral("bbox"), QString::number(boundingBox.topLeft().longitude()) + QLatin1Char(',') + QString::number(boundingBox.bottomRight().latitude()) + QLatin1Char(',') + QString::number(boundingBox.bottomRight().longitude()) + QLatin1Char(',') + QString::number(boundingBox.topLeft().latitude())); } if (request.limit() > 0) queryItems.addQueryItem(QStringLiteral("limit"), QString::number(request.limit())); // XXX: Investigate searchContext() use cases. requestUrl.setQuery(queryItems); QNetworkRequest networkRequest(requestUrl); networkRequest.setHeader(QNetworkRequest::UserAgentHeader, m_userAgent); QNetworkReply *networkReply = m_networkManager->get(networkRequest); QPlaceReply *reply; if (searchType == PlaceSearchType::CompleteSearch) reply = new QPlaceSearchReplyMapbox(request, networkReply, this); else reply = new QPlaceSearchSuggestionReplyMapbox(networkReply, this); connect(reply, &QPlaceReply::finished, this, &QPlaceManagerEngineMapbox::onReplyFinished); connect(reply, QOverload::of(&QPlaceReply::error), this, &QPlaceManagerEngineMapbox::onReplyError); return reply; } QPlaceReply *QPlaceManagerEngineMapbox::initializeCategories() { if (m_categories.isEmpty()) { for (const QString &categoryId : categories) { QPlaceCategory category; category.setName(QMapboxCommon::mapboxNameForCategory(categoryId)); category.setCategoryId(categoryId); category.setVisibility(QLocation::PublicVisibility); m_categories[categoryId] = category; } } QPlaceCategoriesReplyMapbox *reply = new QPlaceCategoriesReplyMapbox(this); connect(reply, &QPlaceReply::finished, this, &QPlaceManagerEngineMapbox::onReplyFinished); connect(reply, QOverload::of(&QPlaceReply::error), this, &QPlaceManagerEngineMapbox::onReplyError); // Queue a future finished() emission from the reply. QMetaObject::invokeMethod(reply, "finish", Qt::QueuedConnection); return reply; } QString QPlaceManagerEngineMapbox::parentCategoryId(const QString &categoryId) const { Q_UNUSED(categoryId); // Only a single category level. return QString(); } QStringList QPlaceManagerEngineMapbox::childCategoryIds(const QString &categoryId) const { // Only a single category level. if (categoryId.isEmpty()) return m_categories.keys(); return QStringList(); } QPlaceCategory QPlaceManagerEngineMapbox::category(const QString &categoryId) const { return m_categories.value(categoryId); } QList QPlaceManagerEngineMapbox::childCategories(const QString &parentId) const { // Only a single category level. if (parentId.isEmpty()) return m_categories.values(); return QList(); } QList QPlaceManagerEngineMapbox::locales() const { return m_locales; } void QPlaceManagerEngineMapbox::setLocales(const QList &locales) { m_locales = locales; } void QPlaceManagerEngineMapbox::onReplyFinished() { QPlaceReply *reply = qobject_cast(sender()); if (reply) emit finished(reply); } void QPlaceManagerEngineMapbox::onReplyError(QPlaceReply::Error errorCode, const QString &errorString) { QPlaceReply *reply = qobject_cast(sender()); if (reply) emit error(reply, errorCode, errorString); }