1/****************************************************************************
2**
3** Copyright (C) 2013-2018 Esri <contracts@esri.com>
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the QtLocation 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 "placemanagerengine_esri.h"
41#include "placesearchreply_esri.h"
42#include "placecategoriesreply_esri.h"
43
44#include <QJsonDocument>
45#include <QJsonObject>
46#include <QJsonArray>
47
48#include <QtCore/QUrlQuery>
49
50QT_BEGIN_NAMESPACE
51
52// https://developers.arcgis.com/rest/geocode/api-reference/geocoding-find-address-candidates.htm
53// https://developers.arcgis.com/rest/geocode/api-reference/geocoding-category-filtering.htm
54// https://developers.arcgis.com/rest/geocode/api-reference/geocoding-service-output.htm
55
56static const QString kCategoriesKey(QStringLiteral("categories"));
57static const QString kSingleLineKey(QStringLiteral("singleLine"));
58static const QString kLocationKey(QStringLiteral("location"));
59static const QString kNameKey(QStringLiteral("name"));
60static const QString kOutFieldsKey(QStringLiteral("outFields"));
61static const QString kCandidateFieldsKey(QStringLiteral("candidateFields"));
62static const QString kCountriesKey(QStringLiteral("detailedCountries"));
63static const QString kLocalizedNamesKey(QStringLiteral("localizedNames"));
64static const QString kMaxLocationsKey(QStringLiteral("maxLocations"));
65
66static const QUrl kUrlGeocodeServer("http://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer?f=pjson");
67static const QUrl kUrlFindAddressCandidates("http://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/findAddressCandidates");
68
69PlaceManagerEngineEsri::PlaceManagerEngineEsri(const QVariantMap &parameters, QGeoServiceProvider::Error *error,
70 QString *errorString) :
71 QPlaceManagerEngine(parameters),
72 m_networkManager(new QNetworkAccessManager(this))
73{
74 *error = QGeoServiceProvider::NoError;
75 errorString->clear();
76}
77
78PlaceManagerEngineEsri::~PlaceManagerEngineEsri()
79{
80}
81
82QList<QLocale> PlaceManagerEngineEsri::locales() const
83{
84 return m_locales;
85}
86
87void PlaceManagerEngineEsri::setLocales(const QList<QLocale> &locales)
88{
89 m_locales = locales;
90}
91
92/***** Search *****/
93
94QPlaceSearchReply *PlaceManagerEngineEsri::search(const QPlaceSearchRequest &request)
95{
96 bool unsupported = false;
97
98 // Only public visibility supported
99 unsupported |= request.visibilityScope() != QLocation::UnspecifiedVisibility &&
100 request.visibilityScope() != QLocation::PublicVisibility;
101 unsupported |= request.searchTerm().isEmpty() && request.categories().isEmpty();
102
103 if (unsupported)
104 return QPlaceManagerEngine::search(request);
105
106 QUrlQuery queryItems;
107 queryItems.addQueryItem(QStringLiteral("f"), QStringLiteral("json"));
108
109 const QGeoCoordinate center = request.searchArea().center();
110 if (center.isValid())
111 {
112 const QString location = QString("%1,%2").arg(a: center.longitude()).arg(a: center.latitude());
113 queryItems.addQueryItem(key: kLocationKey, value: location);
114 }
115
116 const QGeoRectangle boundingBox = request.searchArea().boundingGeoRectangle();
117 if (!boundingBox.isEmpty())
118 {
119 const QString searchExtent = QString("%1,%2,%3,%4")
120 .arg(a: boundingBox.topLeft().longitude())
121 .arg(a: boundingBox.topLeft().latitude())
122 .arg(a: boundingBox.bottomRight().longitude())
123 .arg(a: boundingBox.bottomRight().latitude());
124 queryItems.addQueryItem(QStringLiteral("searchExtent"), value: searchExtent);
125 }
126
127 if (!request.searchTerm().isEmpty())
128 queryItems.addQueryItem(key: kSingleLineKey, value: request.searchTerm());
129
130 QStringList categories;
131 if (!request.categories().isEmpty())
132 {
133 foreach (const QPlaceCategory &placeCategory, request.categories())
134 categories.append(t: placeCategory.categoryId());
135 queryItems.addQueryItem(key: "category", value: categories.join(sep: ","));
136 }
137
138 if (request.limit() > 0)
139 queryItems.addQueryItem(key: kMaxLocationsKey, value: QString::number(request.limit()));
140
141 queryItems.addQueryItem(key: kOutFieldsKey, QStringLiteral("*"));
142
143 QUrl requestUrl(kUrlFindAddressCandidates);
144 requestUrl.setQuery(queryItems);
145
146 QNetworkRequest networkRequest(requestUrl);
147 networkRequest.setAttribute(code: QNetworkRequest::RedirectPolicyAttribute, value: QNetworkRequest::NoLessSafeRedirectPolicy);
148 QNetworkReply *networkReply = m_networkManager->get(request: networkRequest);
149
150 PlaceSearchReplyEsri *reply = new PlaceSearchReplyEsri(request, networkReply, m_candidateFieldsLocale, m_countriesLocale, this);
151 connect(sender: reply, SIGNAL(finished()), receiver: this, SLOT(replyFinished()));
152 connect(sender: reply, SIGNAL(error(QPlaceReply::Error,QString)), receiver: this, SLOT(replyError(QPlaceReply::Error,QString)));
153
154 return reply;
155}
156
157void PlaceManagerEngineEsri::replyFinished()
158{
159 QPlaceReply *reply = qobject_cast<QPlaceReply *>(object: sender());
160 if (reply)
161 emit finished(reply);
162}
163
164void PlaceManagerEngineEsri::replyError(QPlaceReply::Error errorCode, const QString &errorString)
165{
166 QPlaceReply *reply = qobject_cast<QPlaceReply *>(object: sender());
167 if (reply)
168 emit error(reply, error: errorCode, errorString);
169}
170
171/***** Categories *****/
172
173QPlaceReply *PlaceManagerEngineEsri::initializeCategories()
174{
175 initializeGeocodeServer();
176
177 PlaceCategoriesReplyEsri *reply = new PlaceCategoriesReplyEsri(this);
178 connect(sender: reply, SIGNAL(finished()), receiver: this, SLOT(replyFinished()));
179 connect(sender: reply, SIGNAL(error(QPlaceReply::Error,QString)), receiver: this, SLOT(replyError(QPlaceReply::Error,QString)));
180
181 // TODO delayed finished() emission
182 if (!m_categories.isEmpty())
183 reply->emitFinished();
184
185 m_pendingCategoriesReply.append(t: reply);
186 return reply;
187}
188
189void PlaceManagerEngineEsri::parseCategories(const QJsonArray &jsonArray, const QString &parentCategoryId)
190{
191 foreach (const QJsonValue &jsonValue, jsonArray)
192 {
193 if (!jsonValue.isObject())
194 continue;
195
196 const QJsonObject jsonCategory = jsonValue.toObject();
197 const QString key = jsonCategory.value(key: kNameKey).toString();
198 const QString localeName = localizedName(jsonObject: jsonCategory);
199
200 if (key.isEmpty())
201 continue;
202
203 QPlaceCategory category;
204 category.setCategoryId(key);
205 category.setName(localeName.isEmpty() ? key : localeName); // localizedNames
206 m_categories.insert(akey: key, avalue: category);
207 m_subcategories[parentCategoryId].append(t: key);
208 m_parentCategory.insert(akey: key, avalue: parentCategoryId);
209 emit categoryAdded(category, parentCategoryId);
210
211 if (jsonCategory.contains(key: kCategoriesKey))
212 {
213 const QJsonArray jsonArray = jsonCategory.value(key: kCategoriesKey).toArray();
214 parseCategories(jsonArray, parentCategoryId: key);
215 }
216 }
217}
218
219QString PlaceManagerEngineEsri::parentCategoryId(const QString &categoryId) const
220{
221 return m_parentCategory.value(akey: categoryId);
222}
223
224QStringList PlaceManagerEngineEsri::childCategoryIds(const QString &categoryId) const
225{
226 return m_subcategories.value(akey: categoryId);
227}
228
229QPlaceCategory PlaceManagerEngineEsri::category(const QString &categoryId) const
230{
231 return m_categories.value(akey: categoryId);
232}
233
234QList<QPlaceCategory> PlaceManagerEngineEsri::childCategories(const QString &parentId) const
235{
236 QList<QPlaceCategory> categories;
237 foreach (const QString &id, m_subcategories.value(parentId))
238 categories.append(t: m_categories.value(akey: id));
239 return categories;
240}
241
242void PlaceManagerEngineEsri::finishCategories()
243{
244 foreach (PlaceCategoriesReplyEsri *reply, m_pendingCategoriesReply)
245 reply->emitFinished();
246 m_pendingCategoriesReply.clear();
247}
248
249void PlaceManagerEngineEsri::errorCaterogies(const QString &error)
250{
251 foreach (PlaceCategoriesReplyEsri *reply, m_pendingCategoriesReply)
252 reply->setError(errorCode: QPlaceReply::CommunicationError, errorString: error);
253}
254
255/***** GeocodeServer *****/
256
257void PlaceManagerEngineEsri::initializeGeocodeServer()
258{
259 // Only fetch categories once
260 if (m_categories.isEmpty() && !m_geocodeServerReply)
261 {
262 m_geocodeServerReply = m_networkManager->get(request: QNetworkRequest(kUrlGeocodeServer));
263 connect(sender: m_geocodeServerReply, SIGNAL(finished()), receiver: this, SLOT(geocodeServerReplyFinished()));
264 connect(sender: m_geocodeServerReply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), receiver: this, SLOT(geocodeServerReplyError()));
265 }
266}
267
268QString PlaceManagerEngineEsri::localizedName(const QJsonObject &jsonObject)
269{
270 const QJsonObject localizedNames = jsonObject.value(key: kLocalizedNamesKey).toObject();
271
272 foreach (const QLocale &locale, m_locales)
273 {
274 const QString localeStr = locale.name();
275 if (localizedNames.contains(key: localeStr))
276 {
277 return localizedNames.value(key: localeStr).toString();
278 }
279
280 const QString shortLocale = localeStr.left(n: 2);
281 if (localizedNames.contains(key: shortLocale))
282 {
283 return localizedNames.value(key: shortLocale).toString();
284 }
285 }
286 return QString();
287}
288
289void PlaceManagerEngineEsri::parseCandidateFields(const QJsonArray &jsonArray)
290{
291 foreach (const QJsonValue &jsonValue, jsonArray)
292 {
293 if (!jsonValue.isObject())
294 continue;
295
296 const QJsonObject jsonCandidateField = jsonValue.toObject();
297 if (!jsonCandidateField.contains(key: kLocalizedNamesKey))
298 continue;
299
300 const QString key = jsonCandidateField.value(key: kNameKey).toString();
301 m_candidateFieldsLocale.insert(akey: key, avalue: localizedName(jsonObject: jsonCandidateField));
302 }
303}
304
305void PlaceManagerEngineEsri::parseCountries(const QJsonArray &jsonArray)
306{
307 foreach (const QJsonValue &jsonValue, jsonArray)
308 {
309 if (!jsonValue.isObject())
310 continue;
311
312 const QJsonObject jsonCountry = jsonValue.toObject();
313 if (!jsonCountry.contains(key: kLocalizedNamesKey))
314 continue;
315
316 const QString key = jsonCountry.value(key: kNameKey).toString();
317 m_countriesLocale.insert(akey: key, avalue: localizedName(jsonObject: jsonCountry));
318 }
319}
320
321void PlaceManagerEngineEsri::geocodeServerReplyFinished()
322{
323 if (!m_geocodeServerReply)
324 return;
325
326 QJsonDocument document = QJsonDocument::fromJson(json: m_geocodeServerReply->readAll());
327 if (!document.isObject())
328 {
329 errorCaterogies(error: m_geocodeServerReply->errorString());
330 return;
331 }
332
333 QJsonObject jsonObject = document.object();
334
335 // parse categories
336 if (jsonObject.contains(key: kCategoriesKey))
337 {
338 const QJsonArray jsonArray = jsonObject.value(key: kCategoriesKey).toArray();
339 parseCategories(jsonArray, parentCategoryId: QString());
340 }
341
342 // parse candidateFields
343 if (jsonObject.contains(key: kCandidateFieldsKey))
344 {
345 const QJsonArray jsonArray = jsonObject.value(key: kCandidateFieldsKey).toArray();
346 parseCandidateFields(jsonArray);
347 }
348
349 // parse countries
350 if (jsonObject.contains(key: kCountriesKey))
351 {
352 const QJsonArray jsonArray = jsonObject.value(key: kCountriesKey).toArray();
353 parseCountries(jsonArray);
354 }
355
356 finishCategories();
357
358 m_geocodeServerReply->deleteLater();
359}
360
361void PlaceManagerEngineEsri::geocodeServerReplyError()
362{
363 if (m_categories.isEmpty() && !m_geocodeServerReply)
364 return;
365
366 errorCaterogies(error: m_geocodeServerReply->errorString());
367}
368
369QT_END_NAMESPACE
370

source code of qtlocation/src/plugins/geoservices/esri/placemanagerengine_esri.cpp