| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 Aaron McCarthy <mccarthy.aaron@gmail.com> |
| ** 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 "qplacemanagerengineosm.h" |
| #include "qplacesearchreplyosm.h" |
| #include "qplacecategoriesreplyosm.h" |
| |
| #include <QtCore/QUrlQuery> |
| #include <QtCore/QXmlStreamReader> |
| #include <QtCore/QRegularExpression> |
| #include <QtNetwork/QNetworkAccessManager> |
| #include <QtNetwork/QNetworkRequest> |
| #include <QtNetwork/QNetworkReply> |
| #include <QtPositioning/QGeoCircle> |
| #include <QtLocation/private/unsupportedreplies_p.h> |
| |
| #include <QtCore/QElapsedTimer> |
| |
| namespace |
| { |
| QString SpecialPhrasesBaseUrl = QStringLiteral("http://wiki.openstreetmap.org/wiki/Special:Export/Nominatim/Special_Phrases/"); |
| |
| QString nameForTagKey(const QString &tagKey) |
| { |
| if (tagKey == QLatin1String("aeroway")) |
| return QPlaceManagerEngineOsm::tr("Aeroway"); |
| else if (tagKey == QLatin1String("amenity")) |
| return QPlaceManagerEngineOsm::tr("Amenity"); |
| else if (tagKey == QLatin1String("building")) |
| return QPlaceManagerEngineOsm::tr("Building"); |
| else if (tagKey == QLatin1String("highway")) |
| return QPlaceManagerEngineOsm::tr("Highway"); |
| else if (tagKey == QLatin1String("historic")) |
| return QPlaceManagerEngineOsm::tr("Historic"); |
| else if (tagKey == QLatin1String("landuse")) |
| return QPlaceManagerEngineOsm::tr("Land use"); |
| else if (tagKey == QLatin1String("leisure")) |
| return QPlaceManagerEngineOsm::tr("Leisure"); |
| else if (tagKey == QLatin1String("man_made")) |
| return QPlaceManagerEngineOsm::tr("Man made"); |
| else if (tagKey == QLatin1String("natural")) |
| return QPlaceManagerEngineOsm::tr("Natural"); |
| else if (tagKey == QLatin1String("place")) |
| return QPlaceManagerEngineOsm::tr("Place"); |
| else if (tagKey == QLatin1String("railway")) |
| return QPlaceManagerEngineOsm::tr("Railway"); |
| else if (tagKey == QLatin1String("shop")) |
| return QPlaceManagerEngineOsm::tr("Shop"); |
| else if (tagKey == QLatin1String("tourism")) |
| return QPlaceManagerEngineOsm::tr("Tourism"); |
| else if (tagKey == QLatin1String("waterway")) |
| return QPlaceManagerEngineOsm::tr("Waterway"); |
| else |
| return tagKey; |
| } |
| |
| } |
| |
| QPlaceManagerEngineOsm::QPlaceManagerEngineOsm(const QVariantMap ¶meters, |
| QGeoServiceProvider::Error *error, |
| QString *errorString) |
| : QPlaceManagerEngine(parameters), m_networkManager(new QNetworkAccessManager(this)), |
| m_categoriesReply(0) |
| { |
| if (parameters.contains(QStringLiteral("osm.useragent"))) |
| m_userAgent = parameters.value(QStringLiteral("osm.useragent")).toString().toLatin1(); |
| else |
| m_userAgent = "Qt Location based application"; |
| |
| if (parameters.contains(QStringLiteral("osm.places.host"))) |
| m_urlPrefix = parameters.value(QStringLiteral("osm.places.host")).toString(); |
| else |
| m_urlPrefix = QStringLiteral("http://nominatim.openstreetmap.org/search"); |
| |
| |
| if (parameters.contains(QStringLiteral("osm.places.debug_query"))) |
| m_debugQuery = parameters.value(QStringLiteral("osm.places.debug_query")).toBool(); |
| |
| if (parameters.contains(QStringLiteral("osm.places.page_size")) |
| && parameters.value(QStringLiteral("osm.places.page_size")).canConvert<int>()) |
| m_pageSize = parameters.value(QStringLiteral("osm.places.page_size")).toInt(); |
| |
| *error = QGeoServiceProvider::NoError; |
| errorString->clear(); |
| } |
| |
| QPlaceManagerEngineOsm::~QPlaceManagerEngineOsm() |
| { |
| } |
| |
| QPlaceSearchReply *QPlaceManagerEngineOsm::search(const QPlaceSearchRequest &request) |
| { |
| bool unsupported = false; |
| |
| // Only public visibility supported |
| unsupported |= request.visibilityScope() != QLocation::UnspecifiedVisibility && |
| request.visibilityScope() != QLocation::PublicVisibility; |
| unsupported |= request.searchTerm().isEmpty() && request.categories().isEmpty(); |
| |
| if (unsupported) |
| return QPlaceManagerEngine::search(request); |
| |
| QUrlQuery queryItems; |
| |
| queryItems.addQueryItem(QStringLiteral("format"), QStringLiteral("jsonv2")); |
| |
| //queryItems.addQueryItem(QStringLiteral("accept-language"), QStringLiteral("en")); |
| |
| QGeoRectangle boundingBox = request.searchArea().boundingGeoRectangle(); |
| |
| if (!boundingBox.isEmpty()) { |
| queryItems.addQueryItem(QStringLiteral("bounded"), QStringLiteral("1")); |
| QString coordinates; |
| coordinates = QString::number(boundingBox.topLeft().longitude()) + QLatin1Char(',') + |
| QString::number(boundingBox.topLeft().latitude()) + QLatin1Char(',') + |
| QString::number(boundingBox.bottomRight().longitude()) + QLatin1Char(',') + |
| QString::number(boundingBox.bottomRight().latitude()); |
| queryItems.addQueryItem(QStringLiteral("viewbox"), coordinates); |
| } |
| |
| QStringList queryParts; |
| if (!request.searchTerm().isEmpty()) |
| queryParts.append(request.searchTerm()); |
| |
| foreach (const QPlaceCategory &category, request.categories()) { |
| QString id = category.categoryId(); |
| int index = id.indexOf(QLatin1Char('=')); |
| if (index != -1) |
| id = id.mid(index+1); |
| queryParts.append(QLatin1Char('[') + id + QLatin1Char(']')); |
| } |
| |
| queryItems.addQueryItem(QStringLiteral("q"), queryParts.join(QLatin1Char('+'))); |
| |
| QVariantMap parameters = request.searchContext().toMap(); |
| |
| QStringList placeIds = parameters.value(QStringLiteral("ExcludePlaceIds")).toStringList(); |
| if (!placeIds.isEmpty()) |
| queryItems.addQueryItem(QStringLiteral("exclude_place_ids"), placeIds.join(QLatin1Char(','))); |
| |
| queryItems.addQueryItem(QStringLiteral("addressdetails"), QStringLiteral("1")); |
| queryItems.addQueryItem(QStringLiteral("limit"), (request.limit() > 0) ? QString::number(request.limit()) |
| : QString::number(m_pageSize)); |
| |
| QUrl requestUrl(m_urlPrefix); |
| requestUrl.setQuery(queryItems); |
| |
| QNetworkRequest rq(requestUrl); |
| rq.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); |
| QNetworkReply *networkReply = m_networkManager->get(rq); |
| |
| QPlaceSearchReplyOsm *reply = new QPlaceSearchReplyOsm(request, networkReply, this); |
| connect(reply, SIGNAL(finished()), this, SLOT(replyFinished())); |
| connect(reply, SIGNAL(error(QPlaceReply::Error,QString)), |
| this, SLOT(replyError(QPlaceReply::Error,QString))); |
| |
| if (m_debugQuery) |
| reply->requestUrl = requestUrl.url(QUrl::None); |
| |
| return reply; |
| } |
| |
| QPlaceReply *QPlaceManagerEngineOsm::initializeCategories() |
| { |
| // Only fetch categories once |
| if (m_categories.isEmpty() && !m_categoriesReply) { |
| m_categoryLocales = m_locales; |
| m_categoryLocales.append(QLocale(QLocale::English)); |
| fetchNextCategoryLocale(); |
| } |
| |
| QPlaceCategoriesReplyOsm *reply = new QPlaceCategoriesReplyOsm(this); |
| connect(reply, SIGNAL(finished()), this, SLOT(replyFinished())); |
| connect(reply, SIGNAL(error(QPlaceReply::Error,QString)), |
| this, SLOT(replyError(QPlaceReply::Error,QString))); |
| |
| // TODO delayed finished() emission |
| if (!m_categories.isEmpty()) |
| reply->emitFinished(); |
| |
| m_pendingCategoriesReply.append(reply); |
| return reply; |
| } |
| |
| QString QPlaceManagerEngineOsm::parentCategoryId(const QString &categoryId) const |
| { |
| Q_UNUSED(categoryId); |
| |
| // Only a two category levels |
| return QString(); |
| } |
| |
| QStringList QPlaceManagerEngineOsm::childCategoryIds(const QString &categoryId) const |
| { |
| return m_subcategories.value(categoryId); |
| } |
| |
| QPlaceCategory QPlaceManagerEngineOsm::category(const QString &categoryId) const |
| { |
| return m_categories.value(categoryId); |
| } |
| |
| QList<QPlaceCategory> QPlaceManagerEngineOsm::childCategories(const QString &parentId) const |
| { |
| QList<QPlaceCategory> categories; |
| foreach (const QString &id, m_subcategories.value(parentId)) |
| categories.append(m_categories.value(id)); |
| return categories; |
| } |
| |
| QList<QLocale> QPlaceManagerEngineOsm::locales() const |
| { |
| return m_locales; |
| } |
| |
| void QPlaceManagerEngineOsm::setLocales(const QList<QLocale> &locales) |
| { |
| m_locales = locales; |
| } |
| |
| void QPlaceManagerEngineOsm::categoryReplyFinished() |
| { |
| QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender()); |
| reply->deleteLater(); |
| |
| QXmlStreamReader parser(reply); |
| while (!parser.atEnd() && parser.readNextStartElement()) { |
| if (parser.name() == QLatin1String("mediawiki")) |
| continue; |
| if (parser.name() == QLatin1String("page")) |
| continue; |
| if (parser.name() == QLatin1String("revision")) |
| continue; |
| if (parser.name() == QLatin1String("text")) { |
| // parse |
| QString page = parser.readElementText(); |
| QRegularExpression regex(QStringLiteral("\\| ([^|]+) \\|\\| ([^|]+) \\|\\| ([^|]+) \\|\\| ([^|]+) \\|\\| ([\\-YN])")); |
| QRegularExpressionMatchIterator i = regex.globalMatch(page); |
| while (i.hasNext()) { |
| QRegularExpressionMatch match = i.next(); |
| QString name = match.capturedRef(1).toString(); |
| QString tagKey = match.capturedRef(2).toString(); |
| QString tagValue = match.capturedRef(3).toString(); |
| QString op = match.capturedRef(4).toString(); |
| QString plural = match.capturedRef(5).toString(); |
| |
| // Only interested in any operator plural forms |
| if (op != QLatin1String("-") || plural != QLatin1String("Y")) |
| continue; |
| |
| if (!m_categories.contains(tagKey)) { |
| QPlaceCategory category; |
| category.setCategoryId(tagKey); |
| category.setName(nameForTagKey(tagKey)); |
| m_categories.insert(category.categoryId(), category); |
| m_subcategories[QString()].append(tagKey); |
| emit categoryAdded(category, QString()); |
| } |
| |
| QPlaceCategory category; |
| category.setCategoryId(tagKey + QLatin1Char('=') + tagValue); |
| category.setName(name); |
| |
| if (!m_categories.contains(category.categoryId())) { |
| m_categories.insert(category.categoryId(), category); |
| m_subcategories[tagKey].append(category.categoryId()); |
| emit categoryAdded(category, tagKey); |
| } |
| } |
| } |
| |
| parser.skipCurrentElement(); |
| } |
| |
| if (m_categories.isEmpty() && !m_categoryLocales.isEmpty()) { |
| fetchNextCategoryLocale(); |
| return; |
| } else { |
| m_categoryLocales.clear(); |
| } |
| |
| foreach (QPlaceCategoriesReplyOsm *reply, m_pendingCategoriesReply) |
| reply->emitFinished(); |
| m_pendingCategoriesReply.clear(); |
| } |
| |
| void QPlaceManagerEngineOsm::categoryReplyError() |
| { |
| foreach (QPlaceCategoriesReplyOsm *reply, m_pendingCategoriesReply) |
| reply->setError(QPlaceReply::CommunicationError, tr("Network request error")); |
| } |
| |
| void QPlaceManagerEngineOsm::replyFinished() |
| { |
| QPlaceReply *reply = qobject_cast<QPlaceReply *>(sender()); |
| if (reply) |
| emit finished(reply); |
| } |
| |
| void QPlaceManagerEngineOsm::replyError(QPlaceReply::Error errorCode, const QString &errorString) |
| { |
| QPlaceReply *reply = qobject_cast<QPlaceReply *>(sender()); |
| if (reply) |
| emit error(reply, errorCode, errorString); |
| } |
| |
| void QPlaceManagerEngineOsm::fetchNextCategoryLocale() |
| { |
| if (m_categoryLocales.isEmpty()) { |
| qWarning("No locales specified to fetch categories for"); |
| return; |
| } |
| |
| QLocale locale = m_categoryLocales.takeFirst(); |
| |
| // FIXME: Categories should be cached. |
| QUrl requestUrl = QUrl(SpecialPhrasesBaseUrl + locale.name().left(2).toUpper()); |
| |
| m_categoriesReply = m_networkManager->get(QNetworkRequest(requestUrl)); |
| connect(m_categoriesReply, SIGNAL(finished()), this, SLOT(categoryReplyFinished())); |
| connect(m_categoriesReply, SIGNAL(error(QNetworkReply::NetworkError)), |
| this, SLOT(categoryReplyError())); |
| } |