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 &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 
~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