1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 Aaron McCarthy <mccarthy.aaron@gmail.com> |
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 "qplacemanagerengineosm.h" |
41 | #include "qplacesearchreplyosm.h" |
42 | #include "qplacecategoriesreplyosm.h" |
43 | |
44 | #include <QtCore/QUrlQuery> |
45 | #include <QtCore/QXmlStreamReader> |
46 | #include <QtCore/QRegularExpression> |
47 | #include <QtNetwork/QNetworkAccessManager> |
48 | #include <QtNetwork/QNetworkRequest> |
49 | #include <QtNetwork/QNetworkReply> |
50 | #include <QtPositioning/QGeoCircle> |
51 | #include <QtLocation/private/unsupportedreplies_p.h> |
52 | |
53 | #include <QtCore/QElapsedTimer> |
54 | |
55 | namespace |
56 | { |
57 | QString SpecialPhrasesBaseUrl = QStringLiteral("http://wiki.openstreetmap.org/wiki/Special:Export/Nominatim/Special_Phrases/" ); |
58 | |
59 | QString nameForTagKey(const QString &tagKey) |
60 | { |
61 | if (tagKey == QLatin1String("aeroway" )) |
62 | return QPlaceManagerEngineOsm::tr(s: "Aeroway" ); |
63 | else if (tagKey == QLatin1String("amenity" )) |
64 | return QPlaceManagerEngineOsm::tr(s: "Amenity" ); |
65 | else if (tagKey == QLatin1String("building" )) |
66 | return QPlaceManagerEngineOsm::tr(s: "Building" ); |
67 | else if (tagKey == QLatin1String("highway" )) |
68 | return QPlaceManagerEngineOsm::tr(s: "Highway" ); |
69 | else if (tagKey == QLatin1String("historic" )) |
70 | return QPlaceManagerEngineOsm::tr(s: "Historic" ); |
71 | else if (tagKey == QLatin1String("landuse" )) |
72 | return QPlaceManagerEngineOsm::tr(s: "Land use" ); |
73 | else if (tagKey == QLatin1String("leisure" )) |
74 | return QPlaceManagerEngineOsm::tr(s: "Leisure" ); |
75 | else if (tagKey == QLatin1String("man_made" )) |
76 | return QPlaceManagerEngineOsm::tr(s: "Man made" ); |
77 | else if (tagKey == QLatin1String("natural" )) |
78 | return QPlaceManagerEngineOsm::tr(s: "Natural" ); |
79 | else if (tagKey == QLatin1String("place" )) |
80 | return QPlaceManagerEngineOsm::tr(s: "Place" ); |
81 | else if (tagKey == QLatin1String("railway" )) |
82 | return QPlaceManagerEngineOsm::tr(s: "Railway" ); |
83 | else if (tagKey == QLatin1String("shop" )) |
84 | return QPlaceManagerEngineOsm::tr(s: "Shop" ); |
85 | else if (tagKey == QLatin1String("tourism" )) |
86 | return QPlaceManagerEngineOsm::tr(s: "Tourism" ); |
87 | else if (tagKey == QLatin1String("waterway" )) |
88 | return QPlaceManagerEngineOsm::tr(s: "Waterway" ); |
89 | else |
90 | return tagKey; |
91 | } |
92 | |
93 | } |
94 | |
95 | QPlaceManagerEngineOsm::QPlaceManagerEngineOsm(const QVariantMap ¶meters, |
96 | QGeoServiceProvider::Error *error, |
97 | QString *errorString) |
98 | : QPlaceManagerEngine(parameters), m_networkManager(new QNetworkAccessManager(this)), |
99 | m_categoriesReply(0) |
100 | { |
101 | if (parameters.contains(QStringLiteral("osm.useragent" ))) |
102 | m_userAgent = parameters.value(QStringLiteral("osm.useragent" )).toString().toLatin1(); |
103 | else |
104 | m_userAgent = "Qt Location based application" ; |
105 | |
106 | if (parameters.contains(QStringLiteral("osm.places.host" ))) |
107 | m_urlPrefix = parameters.value(QStringLiteral("osm.places.host" )).toString(); |
108 | else |
109 | m_urlPrefix = QStringLiteral("http://nominatim.openstreetmap.org/search" ); |
110 | |
111 | |
112 | if (parameters.contains(QStringLiteral("osm.places.debug_query" ))) |
113 | m_debugQuery = parameters.value(QStringLiteral("osm.places.debug_query" )).toBool(); |
114 | |
115 | if (parameters.contains(QStringLiteral("osm.places.page_size" )) |
116 | && parameters.value(QStringLiteral("osm.places.page_size" )).canConvert<int>()) |
117 | m_pageSize = parameters.value(QStringLiteral("osm.places.page_size" )).toInt(); |
118 | |
119 | *error = QGeoServiceProvider::NoError; |
120 | errorString->clear(); |
121 | } |
122 | |
123 | QPlaceManagerEngineOsm::~QPlaceManagerEngineOsm() |
124 | { |
125 | } |
126 | |
127 | QPlaceSearchReply *QPlaceManagerEngineOsm::search(const QPlaceSearchRequest &request) |
128 | { |
129 | bool unsupported = false; |
130 | |
131 | // Only public visibility supported |
132 | unsupported |= request.visibilityScope() != QLocation::UnspecifiedVisibility && |
133 | request.visibilityScope() != QLocation::PublicVisibility; |
134 | unsupported |= request.searchTerm().isEmpty() && request.categories().isEmpty(); |
135 | |
136 | if (unsupported) |
137 | return QPlaceManagerEngine::search(request); |
138 | |
139 | QUrlQuery queryItems; |
140 | |
141 | queryItems.addQueryItem(QStringLiteral("format" ), QStringLiteral("jsonv2" )); |
142 | |
143 | //queryItems.addQueryItem(QStringLiteral("accept-language"), QStringLiteral("en")); |
144 | |
145 | QGeoRectangle boundingBox = request.searchArea().boundingGeoRectangle(); |
146 | |
147 | if (!boundingBox.isEmpty()) { |
148 | queryItems.addQueryItem(QStringLiteral("bounded" ), QStringLiteral("1" )); |
149 | QString coordinates; |
150 | coordinates = QString::number(boundingBox.topLeft().longitude()) + QLatin1Char(',') + |
151 | QString::number(boundingBox.topLeft().latitude()) + QLatin1Char(',') + |
152 | QString::number(boundingBox.bottomRight().longitude()) + QLatin1Char(',') + |
153 | QString::number(boundingBox.bottomRight().latitude()); |
154 | queryItems.addQueryItem(QStringLiteral("viewbox" ), value: coordinates); |
155 | } |
156 | |
157 | QStringList queryParts; |
158 | if (!request.searchTerm().isEmpty()) |
159 | queryParts.append(t: request.searchTerm()); |
160 | |
161 | foreach (const QPlaceCategory &category, request.categories()) { |
162 | QString id = category.categoryId(); |
163 | int index = id.indexOf(c: QLatin1Char('=')); |
164 | if (index != -1) |
165 | id = id.mid(position: index+1); |
166 | queryParts.append(t: QLatin1Char('[') + id + QLatin1Char(']')); |
167 | } |
168 | |
169 | queryItems.addQueryItem(QStringLiteral("q" ), value: queryParts.join(sep: QLatin1Char('+'))); |
170 | |
171 | QVariantMap parameters = request.searchContext().toMap(); |
172 | |
173 | QStringList placeIds = parameters.value(QStringLiteral("ExcludePlaceIds" )).toStringList(); |
174 | if (!placeIds.isEmpty()) |
175 | queryItems.addQueryItem(QStringLiteral("exclude_place_ids" ), value: placeIds.join(sep: QLatin1Char(','))); |
176 | |
177 | queryItems.addQueryItem(QStringLiteral("addressdetails" ), QStringLiteral("1" )); |
178 | queryItems.addQueryItem(QStringLiteral("limit" ), value: (request.limit() > 0) ? QString::number(request.limit()) |
179 | : QString::number(m_pageSize)); |
180 | |
181 | QUrl requestUrl(m_urlPrefix); |
182 | requestUrl.setQuery(queryItems); |
183 | |
184 | QNetworkRequest rq(requestUrl); |
185 | rq.setAttribute(code: QNetworkRequest::RedirectPolicyAttribute, value: QNetworkRequest::NoLessSafeRedirectPolicy); |
186 | QNetworkReply *networkReply = m_networkManager->get(request: rq); |
187 | |
188 | QPlaceSearchReplyOsm *reply = new QPlaceSearchReplyOsm(request, networkReply, this); |
189 | connect(sender: reply, SIGNAL(finished()), receiver: this, SLOT(replyFinished())); |
190 | connect(sender: reply, SIGNAL(error(QPlaceReply::Error,QString)), |
191 | receiver: this, SLOT(replyError(QPlaceReply::Error,QString))); |
192 | |
193 | if (m_debugQuery) |
194 | reply->requestUrl = requestUrl.url(options: QUrl::None); |
195 | |
196 | return reply; |
197 | } |
198 | |
199 | QPlaceReply *QPlaceManagerEngineOsm::initializeCategories() |
200 | { |
201 | // Only fetch categories once |
202 | if (m_categories.isEmpty() && !m_categoriesReply) { |
203 | m_categoryLocales = m_locales; |
204 | m_categoryLocales.append(t: QLocale(QLocale::English)); |
205 | fetchNextCategoryLocale(); |
206 | } |
207 | |
208 | QPlaceCategoriesReplyOsm *reply = new QPlaceCategoriesReplyOsm(this); |
209 | connect(sender: reply, SIGNAL(finished()), receiver: this, SLOT(replyFinished())); |
210 | connect(sender: reply, SIGNAL(error(QPlaceReply::Error,QString)), |
211 | receiver: this, SLOT(replyError(QPlaceReply::Error,QString))); |
212 | |
213 | // TODO delayed finished() emission |
214 | if (!m_categories.isEmpty()) |
215 | reply->emitFinished(); |
216 | |
217 | m_pendingCategoriesReply.append(t: reply); |
218 | return reply; |
219 | } |
220 | |
221 | QString QPlaceManagerEngineOsm::parentCategoryId(const QString &categoryId) const |
222 | { |
223 | Q_UNUSED(categoryId); |
224 | |
225 | // Only a two category levels |
226 | return QString(); |
227 | } |
228 | |
229 | QStringList QPlaceManagerEngineOsm::childCategoryIds(const QString &categoryId) const |
230 | { |
231 | return m_subcategories.value(akey: categoryId); |
232 | } |
233 | |
234 | QPlaceCategory QPlaceManagerEngineOsm::category(const QString &categoryId) const |
235 | { |
236 | return m_categories.value(akey: categoryId); |
237 | } |
238 | |
239 | QList<QPlaceCategory> QPlaceManagerEngineOsm::childCategories(const QString &parentId) const |
240 | { |
241 | QList<QPlaceCategory> categories; |
242 | foreach (const QString &id, m_subcategories.value(parentId)) |
243 | categories.append(t: m_categories.value(akey: id)); |
244 | return categories; |
245 | } |
246 | |
247 | QList<QLocale> QPlaceManagerEngineOsm::locales() const |
248 | { |
249 | return m_locales; |
250 | } |
251 | |
252 | void QPlaceManagerEngineOsm::setLocales(const QList<QLocale> &locales) |
253 | { |
254 | m_locales = locales; |
255 | } |
256 | |
257 | void QPlaceManagerEngineOsm::categoryReplyFinished() |
258 | { |
259 | QNetworkReply *reply = qobject_cast<QNetworkReply *>(object: sender()); |
260 | reply->deleteLater(); |
261 | |
262 | QXmlStreamReader parser(reply); |
263 | while (!parser.atEnd() && parser.readNextStartElement()) { |
264 | if (parser.name() == QLatin1String("mediawiki" )) |
265 | continue; |
266 | if (parser.name() == QLatin1String("page" )) |
267 | continue; |
268 | if (parser.name() == QLatin1String("revision" )) |
269 | continue; |
270 | if (parser.name() == QLatin1String("text" )) { |
271 | // parse |
272 | QString page = parser.readElementText(); |
273 | QRegularExpression regex(QStringLiteral("\\| ([^|]+) \\|\\| ([^|]+) \\|\\| ([^|]+) \\|\\| ([^|]+) \\|\\| ([\\-YN])" )); |
274 | QRegularExpressionMatchIterator i = regex.globalMatch(subject: page); |
275 | while (i.hasNext()) { |
276 | QRegularExpressionMatch match = i.next(); |
277 | QString name = match.capturedRef(nth: 1).toString(); |
278 | QString tagKey = match.capturedRef(nth: 2).toString(); |
279 | QString tagValue = match.capturedRef(nth: 3).toString(); |
280 | QString op = match.capturedRef(nth: 4).toString(); |
281 | QString plural = match.capturedRef(nth: 5).toString(); |
282 | |
283 | // Only interested in any operator plural forms |
284 | if (op != QLatin1String("-" ) || plural != QLatin1String("Y" )) |
285 | continue; |
286 | |
287 | if (!m_categories.contains(akey: tagKey)) { |
288 | QPlaceCategory category; |
289 | category.setCategoryId(tagKey); |
290 | category.setName(nameForTagKey(tagKey)); |
291 | m_categories.insert(akey: category.categoryId(), avalue: category); |
292 | m_subcategories[QString()].append(t: tagKey); |
293 | emit categoryAdded(category, parentCategoryId: QString()); |
294 | } |
295 | |
296 | QPlaceCategory category; |
297 | category.setCategoryId(tagKey + QLatin1Char('=') + tagValue); |
298 | category.setName(name); |
299 | |
300 | if (!m_categories.contains(akey: category.categoryId())) { |
301 | m_categories.insert(akey: category.categoryId(), avalue: category); |
302 | m_subcategories[tagKey].append(t: category.categoryId()); |
303 | emit categoryAdded(category, parentCategoryId: tagKey); |
304 | } |
305 | } |
306 | } |
307 | |
308 | parser.skipCurrentElement(); |
309 | } |
310 | |
311 | if (m_categories.isEmpty() && !m_categoryLocales.isEmpty()) { |
312 | fetchNextCategoryLocale(); |
313 | return; |
314 | } else { |
315 | m_categoryLocales.clear(); |
316 | } |
317 | |
318 | foreach (QPlaceCategoriesReplyOsm *reply, m_pendingCategoriesReply) |
319 | reply->emitFinished(); |
320 | m_pendingCategoriesReply.clear(); |
321 | } |
322 | |
323 | void QPlaceManagerEngineOsm::categoryReplyError() |
324 | { |
325 | foreach (QPlaceCategoriesReplyOsm *reply, m_pendingCategoriesReply) |
326 | reply->setError(errorCode: QPlaceReply::CommunicationError, errorString: tr(s: "Network request error" )); |
327 | } |
328 | |
329 | void QPlaceManagerEngineOsm::replyFinished() |
330 | { |
331 | QPlaceReply *reply = qobject_cast<QPlaceReply *>(object: sender()); |
332 | if (reply) |
333 | emit finished(reply); |
334 | } |
335 | |
336 | void QPlaceManagerEngineOsm::replyError(QPlaceReply::Error errorCode, const QString &errorString) |
337 | { |
338 | QPlaceReply *reply = qobject_cast<QPlaceReply *>(object: sender()); |
339 | if (reply) |
340 | emit error(reply, error: errorCode, errorString); |
341 | } |
342 | |
343 | void QPlaceManagerEngineOsm::fetchNextCategoryLocale() |
344 | { |
345 | if (m_categoryLocales.isEmpty()) { |
346 | qWarning(msg: "No locales specified to fetch categories for" ); |
347 | return; |
348 | } |
349 | |
350 | QLocale locale = m_categoryLocales.takeFirst(); |
351 | |
352 | // FIXME: Categories should be cached. |
353 | QUrl requestUrl = QUrl(SpecialPhrasesBaseUrl + locale.name().left(n: 2).toUpper()); |
354 | |
355 | m_categoriesReply = m_networkManager->get(request: QNetworkRequest(requestUrl)); |
356 | connect(sender: m_categoriesReply, SIGNAL(finished()), receiver: this, SLOT(categoryReplyFinished())); |
357 | connect(sender: m_categoriesReply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), |
358 | receiver: this, SLOT(categoryReplyError())); |
359 | } |
360 | |