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
50 QT_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
56 static const QString kCategoriesKey(QStringLiteral("categories"));
57 static const QString kSingleLineKey(QStringLiteral("singleLine"));
58 static const QString kLocationKey(QStringLiteral("location"));
59 static const QString kNameKey(QStringLiteral("name"));
60 static const QString kOutFieldsKey(QStringLiteral("outFields"));
61 static const QString kCandidateFieldsKey(QStringLiteral("candidateFields"));
62 static const QString kCountriesKey(QStringLiteral("detailedCountries"));
63 static const QString kLocalizedNamesKey(QStringLiteral("localizedNames"));
64 static const QString kMaxLocationsKey(QStringLiteral("maxLocations"));
65
66 static const QUrl kUrlGeocodeServer("http://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer?f=pjson");
67 static const QUrl kUrlFindAddressCandidates("http://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/findAddressCandidates");
68
PlaceManagerEngineEsri(const QVariantMap & parameters,QGeoServiceProvider::Error * error,QString * errorString)69 PlaceManagerEngineEsri::PlaceManagerEngineEsri(const QVariantMap ¶meters, 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
~PlaceManagerEngineEsri()78 PlaceManagerEngineEsri::~PlaceManagerEngineEsri()
79 {
80 }
81
locales() const82 QList<QLocale> PlaceManagerEngineEsri::locales() const
83 {
84 return m_locales;
85 }
86
setLocales(const QList<QLocale> & locales)87 void PlaceManagerEngineEsri::setLocales(const QList<QLocale> &locales)
88 {
89 m_locales = locales;
90 }
91
92 /***** Search *****/
93
search(const QPlaceSearchRequest & request)94 QPlaceSearchReply *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(center.longitude()).arg(center.latitude());
113 queryItems.addQueryItem(kLocationKey, 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(boundingBox.topLeft().longitude())
121 .arg(boundingBox.topLeft().latitude())
122 .arg(boundingBox.bottomRight().longitude())
123 .arg(boundingBox.bottomRight().latitude());
124 queryItems.addQueryItem(QStringLiteral("searchExtent"), searchExtent);
125 }
126
127 if (!request.searchTerm().isEmpty())
128 queryItems.addQueryItem(kSingleLineKey, request.searchTerm());
129
130 QStringList categories;
131 if (!request.categories().isEmpty())
132 {
133 foreach (const QPlaceCategory &placeCategory, request.categories())
134 categories.append(placeCategory.categoryId());
135 queryItems.addQueryItem("category", categories.join(","));
136 }
137
138 if (request.limit() > 0)
139 queryItems.addQueryItem(kMaxLocationsKey, QString::number(request.limit()));
140
141 queryItems.addQueryItem(kOutFieldsKey, QStringLiteral("*"));
142
143 QUrl requestUrl(kUrlFindAddressCandidates);
144 requestUrl.setQuery(queryItems);
145
146 QNetworkRequest networkRequest(requestUrl);
147 networkRequest.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
148 QNetworkReply *networkReply = m_networkManager->get(networkRequest);
149
150 PlaceSearchReplyEsri *reply = new PlaceSearchReplyEsri(request, networkReply, m_candidateFieldsLocale, m_countriesLocale, this);
151 connect(reply, SIGNAL(finished()), this, SLOT(replyFinished()));
152 connect(reply, SIGNAL(error(QPlaceReply::Error,QString)), this, SLOT(replyError(QPlaceReply::Error,QString)));
153
154 return reply;
155 }
156
replyFinished()157 void PlaceManagerEngineEsri::replyFinished()
158 {
159 QPlaceReply *reply = qobject_cast<QPlaceReply *>(sender());
160 if (reply)
161 emit finished(reply);
162 }
163
replyError(QPlaceReply::Error errorCode,const QString & errorString)164 void PlaceManagerEngineEsri::replyError(QPlaceReply::Error errorCode, const QString &errorString)
165 {
166 QPlaceReply *reply = qobject_cast<QPlaceReply *>(sender());
167 if (reply)
168 emit error(reply, errorCode, errorString);
169 }
170
171 /***** Categories *****/
172
initializeCategories()173 QPlaceReply *PlaceManagerEngineEsri::initializeCategories()
174 {
175 initializeGeocodeServer();
176
177 PlaceCategoriesReplyEsri *reply = new PlaceCategoriesReplyEsri(this);
178 connect(reply, SIGNAL(finished()), this, SLOT(replyFinished()));
179 connect(reply, SIGNAL(error(QPlaceReply::Error,QString)), 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(reply);
186 return reply;
187 }
188
parseCategories(const QJsonArray & jsonArray,const QString & parentCategoryId)189 void 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(kNameKey).toString();
198 const QString localeName = localizedName(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(key, category);
207 m_subcategories[parentCategoryId].append(key);
208 m_parentCategory.insert(key, parentCategoryId);
209 emit categoryAdded(category, parentCategoryId);
210
211 if (jsonCategory.contains(kCategoriesKey))
212 {
213 const QJsonArray jsonArray = jsonCategory.value(kCategoriesKey).toArray();
214 parseCategories(jsonArray, key);
215 }
216 }
217 }
218
parentCategoryId(const QString & categoryId) const219 QString PlaceManagerEngineEsri::parentCategoryId(const QString &categoryId) const
220 {
221 return m_parentCategory.value(categoryId);
222 }
223
childCategoryIds(const QString & categoryId) const224 QStringList PlaceManagerEngineEsri::childCategoryIds(const QString &categoryId) const
225 {
226 return m_subcategories.value(categoryId);
227 }
228
category(const QString & categoryId) const229 QPlaceCategory PlaceManagerEngineEsri::category(const QString &categoryId) const
230 {
231 return m_categories.value(categoryId);
232 }
233
childCategories(const QString & parentId) const234 QList<QPlaceCategory> PlaceManagerEngineEsri::childCategories(const QString &parentId) const
235 {
236 QList<QPlaceCategory> categories;
237 foreach (const QString &id, m_subcategories.value(parentId))
238 categories.append(m_categories.value(id));
239 return categories;
240 }
241
finishCategories()242 void PlaceManagerEngineEsri::finishCategories()
243 {
244 foreach (PlaceCategoriesReplyEsri *reply, m_pendingCategoriesReply)
245 reply->emitFinished();
246 m_pendingCategoriesReply.clear();
247 }
248
errorCaterogies(const QString & error)249 void PlaceManagerEngineEsri::errorCaterogies(const QString &error)
250 {
251 foreach (PlaceCategoriesReplyEsri *reply, m_pendingCategoriesReply)
252 reply->setError(QPlaceReply::CommunicationError, error);
253 }
254
255 /***** GeocodeServer *****/
256
initializeGeocodeServer()257 void PlaceManagerEngineEsri::initializeGeocodeServer()
258 {
259 // Only fetch categories once
260 if (m_categories.isEmpty() && !m_geocodeServerReply)
261 {
262 m_geocodeServerReply = m_networkManager->get(QNetworkRequest(kUrlGeocodeServer));
263 connect(m_geocodeServerReply, SIGNAL(finished()), this, SLOT(geocodeServerReplyFinished()));
264 connect(m_geocodeServerReply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(geocodeServerReplyError()));
265 }
266 }
267
localizedName(const QJsonObject & jsonObject)268 QString PlaceManagerEngineEsri::localizedName(const QJsonObject &jsonObject)
269 {
270 const QJsonObject localizedNames = jsonObject.value(kLocalizedNamesKey).toObject();
271
272 foreach (const QLocale &locale, m_locales)
273 {
274 const QString localeStr = locale.name();
275 if (localizedNames.contains(localeStr))
276 {
277 return localizedNames.value(localeStr).toString();
278 }
279
280 const QString shortLocale = localeStr.left(2);
281 if (localizedNames.contains(shortLocale))
282 {
283 return localizedNames.value(shortLocale).toString();
284 }
285 }
286 return QString();
287 }
288
parseCandidateFields(const QJsonArray & jsonArray)289 void 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(kLocalizedNamesKey))
298 continue;
299
300 const QString key = jsonCandidateField.value(kNameKey).toString();
301 m_candidateFieldsLocale.insert(key, localizedName(jsonCandidateField));
302 }
303 }
304
parseCountries(const QJsonArray & jsonArray)305 void 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(kLocalizedNamesKey))
314 continue;
315
316 const QString key = jsonCountry.value(kNameKey).toString();
317 m_countriesLocale.insert(key, localizedName(jsonCountry));
318 }
319 }
320
geocodeServerReplyFinished()321 void PlaceManagerEngineEsri::geocodeServerReplyFinished()
322 {
323 if (!m_geocodeServerReply)
324 return;
325
326 QJsonDocument document = QJsonDocument::fromJson(m_geocodeServerReply->readAll());
327 if (!document.isObject())
328 {
329 errorCaterogies(m_geocodeServerReply->errorString());
330 return;
331 }
332
333 QJsonObject jsonObject = document.object();
334
335 // parse categories
336 if (jsonObject.contains(kCategoriesKey))
337 {
338 const QJsonArray jsonArray = jsonObject.value(kCategoriesKey).toArray();
339 parseCategories(jsonArray, QString());
340 }
341
342 // parse candidateFields
343 if (jsonObject.contains(kCandidateFieldsKey))
344 {
345 const QJsonArray jsonArray = jsonObject.value(kCandidateFieldsKey).toArray();
346 parseCandidateFields(jsonArray);
347 }
348
349 // parse countries
350 if (jsonObject.contains(kCountriesKey))
351 {
352 const QJsonArray jsonArray = jsonObject.value(kCountriesKey).toArray();
353 parseCountries(jsonArray);
354 }
355
356 finishCategories();
357
358 m_geocodeServerReply->deleteLater();
359 }
360
geocodeServerReplyError()361 void PlaceManagerEngineEsri::geocodeServerReplyError()
362 {
363 if (m_categories.isEmpty() && !m_geocodeServerReply)
364 return;
365
366 errorCaterogies(m_geocodeServerReply->errorString());
367 }
368
369 QT_END_NAMESPACE
370