File indexing completed on 2025-01-19 03:53:03
0001 /* ============================================================ 0002 * 0003 * This file is a part of digiKam project 0004 * https://www.digikam.org 0005 * 0006 * Date : 2021-03-20 0007 * Description : a tool to export images to iNaturalist web service 0008 * 0009 * SPDX-FileCopyrightText: 2021-2022 by Joerg Lohse <joergmlpts at gmail dot com> 0010 * 0011 * SPDX-License-Identifier: GPL-2.0-or-later 0012 * 0013 * ============================================================ */ 0014 0015 #include "inattalker.h" 0016 0017 // Qt includes 0018 0019 #include <QByteArray> 0020 #include <QMap> 0021 #include <QUrlQuery> 0022 #include <QHttpMultiPart> 0023 #include <QStringList> 0024 #include <QMessageBox> 0025 #include <QApplication> 0026 #include <QProgressDialog> 0027 #include <QJsonDocument> 0028 #include <QJsonObject> 0029 #include <QJsonArray> 0030 #include <QTimer> 0031 0032 // KDE includes 0033 0034 #include <klocalizedstring.h> 0035 0036 // Local includes 0037 0038 #include "wstoolutils.h" 0039 #include "inatutils.h" 0040 #include "digikam_debug.h" 0041 #include "digikam_config.h" 0042 #include "digikam_version.h" 0043 #include "previewloadthread.h" 0044 #include "inatbrowserdlg.h" 0045 #include "networkmanager.h" 0046 0047 // OAuth2 library includes 0048 0049 #if defined(Q_CC_CLANG) 0050 # pragma clang diagnostic push 0051 # pragma clang diagnostic ignored "-Wextra-semi" 0052 #endif 0053 0054 #include "o0globals.h" 0055 #include "o0settingsstore.h" 0056 0057 #if defined(Q_CC_CLANG) 0058 # pragma clang diagnostic pop 0059 #endif 0060 0061 namespace DigikamGenericINatPlugin 0062 { 0063 0064 static const char COOKIE_SEPARATOR = '\n'; 0065 0066 enum 0067 { 0068 INAT_API_TOKEN_EXPIRATION = 86000, ///< api tokens are valid for 24 hours 0069 GEOLOCATION_PRECISION = 8, ///< # digits after decimal point 0070 RADIUS_PRECISION = 6, ///< # digits after decimal point 0071 EARTH_RADIUS_KM = 6371, ///< Earth radius in kilometers 0072 TIMEOUT_TIMER_RESOLUTION_SECS = 30, ///< timeout check every 30 seconds 0073 RESPONSE_TIMEOUT_SECS = 300, ///< network timeout after this many seconds 0074 MAX_RETRIES = 5 ///< retry network requests this many times 0075 }; 0076 0077 /** 0078 * Fields in server responses received from iNaturalist. 0079 */ 0080 static const QString API_TOKEN = QLatin1String("api_token"); 0081 static const QString TOTAL_RESULTS = QLatin1String("total_results"); 0082 static const QString PAGE = QLatin1String("page"); 0083 static const QString PER_PAGE = QLatin1String("per_page"); 0084 static const QString LOCALE = QLatin1String("locale"); 0085 static const QString RESULTS = QLatin1String("results"); 0086 static const QString NAME = QLatin1String("name"); 0087 static const QString TAXON = QLatin1String("taxon"); 0088 static const QString TAXON_ID = QLatin1String("taxon_id"); 0089 static const QString ID = QLatin1String("id"); 0090 static const QString PARENT_ID = QLatin1String("parent_id"); 0091 static const QString RANK = QLatin1String("rank"); 0092 static const QString RANK_LEVEL = QLatin1String("rank_level"); 0093 static const QString PREFERRED_COMMON_NAME = QLatin1String("preferred_common_name"); 0094 static const QString ENGLISH_COMMON_NAME = QLatin1String("english_common_name"); 0095 static const QString MATCHED_TERM = QLatin1String("matched_term"); 0096 static const QString DEFAULT_PHOTO = QLatin1String("default_photo"); 0097 static const QString SQUARE_URL = QLatin1String("square_url"); 0098 static const QString ANCESTORS = QLatin1String("ancestors"); 0099 static const QString OBSCURED = QLatin1String("obscured"); 0100 static const QString GEOJSON = QLatin1String("geojson"); 0101 static const QString COORDINATES = QLatin1String("coordinates"); 0102 static const QString LOGIN = QLatin1String("login"); 0103 static const QString ICON = QLatin1String("icon"); 0104 static const QString OBSERVATION = QLatin1String("observation"); 0105 static const QString OBSERVATIONS = QLatin1String("observations"); 0106 static const QString OBSERVED_ON = QLatin1String("observed_on"); 0107 static const QString OBSERVED_ON_STRING = QLatin1String("observed_on_string"); 0108 static const QString OBSERVATION_PHOTOS = QLatin1String("observation_photos"); 0109 static const QString PHOTO = QLatin1String("photo"); 0110 0111 static QJsonObject parseJsonResponse(const QByteArray& data) 0112 { 0113 QJsonParseError err; 0114 QJsonDocument doc = QJsonDocument::fromJson(data, &err); 0115 0116 if (err.error != QJsonParseError::NoError) 0117 { 0118 qCWarning(DIGIKAM_WEBSERVICES_LOG) << "parseJsonResponse: Failed to parse json response:" 0119 << err.errorString(); 0120 0121 return QJsonObject(); 0122 } 0123 0124 if (!doc.isObject()) 0125 { 0126 qCWarning(DIGIKAM_WEBSERVICES_LOG) << "parseJsonResponse: Json response is not an object!"; 0127 0128 return QJsonObject(); 0129 } 0130 0131 return doc.object(); 0132 } 0133 0134 static Taxon parseTaxon(const QJsonObject& taxon) 0135 { 0136 int id = -1; 0137 int parentId = -1; 0138 double rankLevel = -1.0; 0139 QString name; 0140 QString rank; 0141 QString commonName; 0142 QString matchedTerm; 0143 QUrl squareUrl; 0144 QList<Taxon> ancestors; 0145 0146 if (taxon.contains(NAME)) 0147 { 0148 name = taxon[NAME].toString(); 0149 } 0150 0151 if (taxon.contains(ID)) 0152 { 0153 id = taxon[ID].toInt(); 0154 } 0155 0156 if (taxon.contains(PARENT_ID)) 0157 { 0158 parentId = taxon[PARENT_ID].toInt(); 0159 } 0160 0161 if (taxon.contains(RANK)) 0162 { 0163 rank = taxon[RANK].toString(); 0164 } 0165 0166 if (taxon.contains(RANK_LEVEL)) 0167 { 0168 rankLevel = taxon[RANK_LEVEL].toDouble(); 0169 } 0170 0171 if (taxon.contains(PREFERRED_COMMON_NAME)) 0172 { 0173 commonName = taxon[PREFERRED_COMMON_NAME].toString(); 0174 } 0175 else if (isEnglish && taxon.contains(ENGLISH_COMMON_NAME)) 0176 { 0177 commonName = taxon[ENGLISH_COMMON_NAME].toString(); 0178 } 0179 0180 if (taxon.contains(MATCHED_TERM)) 0181 { 0182 matchedTerm = taxon[MATCHED_TERM].toString(); 0183 } 0184 0185 if (taxon.contains(DEFAULT_PHOTO) && 0186 taxon[DEFAULT_PHOTO].toObject().contains(SQUARE_URL)) 0187 { 0188 squareUrl = QUrl(taxon[DEFAULT_PHOTO].toObject() 0189 [SQUARE_URL].toString()); 0190 } 0191 0192 if (taxon.contains(ANCESTORS)) 0193 { 0194 for (const auto& ancestorTaxon : taxon[ANCESTORS].toArray()) 0195 { 0196 ancestors << parseTaxon(ancestorTaxon.toObject()); 0197 } 0198 } 0199 0200 return Taxon(id, parentId, name, rank, rankLevel, commonName, 0201 matchedTerm, squareUrl, ancestors); 0202 } 0203 0204 // ------------------------------------------------------------------------------------------ 0205 0206 /** 0207 * A request consists in a state and a function that is called with the response. 0208 */ 0209 class Q_DECL_HIDDEN Request 0210 { 0211 public: 0212 0213 Request() 0214 : m_startTime(QDateTime::currentMSecsSinceEpoch()) 0215 { 0216 } 0217 0218 virtual ~Request() 0219 { 0220 } 0221 0222 static bool networkErrorRetry(QNetworkReply::NetworkError code) 0223 { 0224 return (code == QNetworkReply::ConnectionRefusedError || 0225 code == QNetworkReply::RemoteHostClosedError || 0226 code == QNetworkReply::HostNotFoundError || 0227 code == QNetworkReply::TimeoutError || 0228 code == QNetworkReply::TemporaryNetworkFailureError || 0229 code == QNetworkReply::NetworkSessionFailedError || 0230 code == QNetworkReply::InternalServerError || 0231 code == QNetworkReply::ServiceUnavailableError || 0232 code == QNetworkReply::UnknownServerError); 0233 } 0234 0235 virtual void reportError(INatTalker&, QNetworkReply::NetworkError, 0236 const QString& errorString) const = 0; 0237 0238 virtual void parseResponse(INatTalker& talker, 0239 const QByteArray& data) const = 0; 0240 0241 // Has operation timed out? 0242 0243 bool isTimeout() const 0244 { 0245 return (durationMilliSecs() > 1000 * RESPONSE_TIMEOUT_SECS); 0246 } 0247 0248 // How long did it take? 0249 0250 qint64 durationMilliSecs() const 0251 { 0252 return (QDateTime::currentMSecsSinceEpoch() - m_startTime); 0253 } 0254 0255 private: 0256 0257 qint64 m_startTime; 0258 0259 private: 0260 0261 Q_DISABLE_COPY(Request) 0262 }; 0263 0264 // -------------------------------------------------------------------------------- 0265 0266 class Q_DECL_HIDDEN INatTalker::Private 0267 { 0268 public: 0269 0270 explicit Private() 0271 : parent (nullptr), 0272 netMngr (nullptr), 0273 timer (nullptr), 0274 settings (nullptr), 0275 iface (nullptr), 0276 store (nullptr), 0277 apiTokenExpires (0) 0278 { 0279 QString cryptId(QLatin1String("119b0b8a57644341fe03eca486a341")); 0280 0281 apiUrl = QLatin1String("https://api.inaturalist.org/v1/"); 0282 keyToken = QString(QLatin1String(O2_KEY_TOKEN)).arg(cryptId); 0283 keyExpires = QString(QLatin1String(O2_KEY_EXPIRES)).arg(cryptId); 0284 keyCookies = QString(QLatin1String("cookies.%1")).arg(cryptId); 0285 } 0286 0287 void clear() 0288 { 0289 apiTokenExpires = 0; 0290 apiToken = QString(); 0291 } 0292 0293 QWidget* parent; 0294 QNetworkAccessManager* netMngr; 0295 QTimer* timer; 0296 QSettings* settings; 0297 DInfoInterface* iface; 0298 O0SettingsStore* store; 0299 0300 QString serviceName; 0301 QString apiUrl; 0302 0303 /// keys used in O0SettingsStore 0304 QString keyToken; 0305 QString keyExpires; 0306 QString keyCookies; 0307 0308 /// the api token and its expiration time in seconds since January 1st, 1970 0309 QString apiToken; 0310 uint apiTokenExpires; 0311 0312 /// this hash table allows us to serve multiple requests concurrently 0313 QHash<QNetworkReply*, Request*> pendingRequests; 0314 0315 /// cached api call results 0316 QHash<QString, AutoCompletions> cachedAutoCompletions; 0317 QHash<QUrl, QByteArray> cachedLoadUrls; 0318 QHash<QString, ImageScores> cachedImageScores; 0319 QHash<QString, QStringList> cachedNearbyPlaces; 0320 QHash<QString, NearbyObservation> cachedNearbyObservations; 0321 }; 0322 0323 INatTalker::INatTalker(QWidget* const parent, const QString& serviceName, 0324 DInfoInterface* const iface) 0325 : d(new Private) 0326 { 0327 d->parent = parent; 0328 d->serviceName = serviceName; 0329 d->iface = iface; 0330 m_authProgressDlg = nullptr; 0331 0332 d->netMngr = NetworkManager::instance()->getNetworkManager(this); 0333 d->timer = new QTimer(this); 0334 0335 connect(d->netMngr, SIGNAL(finished(QNetworkReply*)), 0336 this, SLOT(slotFinished(QNetworkReply*))); 0337 0338 connect(d->timer, SIGNAL(timeout()), 0339 this, SLOT(slotTimeout())); 0340 0341 d->settings = WSToolUtils::getOauthSettings(this); 0342 d->store = new O0SettingsStore(d->settings, 0343 QLatin1String(O2_ENCRYPTION_KEY), this); 0344 d->store->setGroupKey(d->serviceName); 0345 d->timer->start(TIMEOUT_TIMER_RESOLUTION_SECS * 1000); 0346 } 0347 0348 INatTalker::~INatTalker() 0349 { 0350 d->timer->stop(); 0351 d->clear(); 0352 WSToolUtils::removeTemporaryDir(d->serviceName.toLatin1().constData()); 0353 0354 delete d; 0355 } 0356 0357 /** 0358 * Try to restore a valid API token; they are good for 24 hours. 0359 */ 0360 bool INatTalker::restoreApiToken(const QString& username, 0361 QList<QNetworkCookie>& cookies, 0362 bool emitSignal) 0363 { 0364 cookies.clear(); 0365 0366 if (username.isEmpty()) 0367 { 0368 return false; 0369 } 0370 0371 d->store->setGroupKey(d->serviceName + username); 0372 d->apiToken = d->store->value(d->keyToken); 0373 d->apiTokenExpires = d->store->value(d->keyExpires, 0374 QString::number(0)).toInt(); 0375 QString cookiesStr = d->store->value(d->keyCookies); 0376 0377 if (!cookiesStr.isEmpty()) 0378 { 0379 QDateTime now(QDateTime::currentDateTime()); 0380 0381 for (const auto& str : cookiesStr.split(QLatin1Char(COOKIE_SEPARATOR))) 0382 { 0383 QList<QNetworkCookie> lst(QNetworkCookie::parseCookies(str.toUtf8())); 0384 0385 Q_ASSERT(lst.count() == 1); 0386 0387 for (const auto& cookie : lst) 0388 { 0389 if (INatBrowserDlg::filterCookie(cookie, true, now)) 0390 { 0391 cookies << cookie; 0392 } 0393 } 0394 } 0395 } 0396 0397 bool valid = (emitSignal && (apiTokenExpiresIn() > 0)); 0398 0399 if (valid) 0400 { 0401 userInfo(cookies); 0402 } 0403 0404 return valid; 0405 } 0406 0407 bool INatTalker::stillUploading() const 0408 { 0409 return d->pendingRequests.count(); 0410 } 0411 0412 void INatTalker::unLink() 0413 { 0414 d->clear(); 0415 } 0416 0417 void INatTalker::removeUserName(const QString& userName) 0418 { 0419 if (userName.startsWith(d->serviceName)) 0420 { 0421 d->settings->beginGroup(userName); 0422 d->settings->remove(QString()); 0423 d->settings->endGroup(); 0424 } 0425 } 0426 0427 int INatTalker::apiTokenExpiresIn() const 0428 { 0429 if (d->apiToken.isEmpty()) 0430 { 0431 return -1; 0432 } 0433 0434 uint time = (uint)(QDateTime::currentMSecsSinceEpoch() / 1000); 0435 0436 return ( 0437 (d->apiTokenExpires <= time) ? -1 0438 : (d->apiTokenExpires - time) 0439 ); 0440 } 0441 0442 /** 0443 * We received an api token from a web browser login. 0444 */ 0445 void INatTalker::slotApiToken(const QString& apiToken, 0446 const QList<QNetworkCookie>& cookies) 0447 { 0448 d->apiToken = apiToken; 0449 0450 if (apiToken.isEmpty()) 0451 { 0452 Q_EMIT signalLinkingFailed(QLatin1String("no api token")); 0453 return; 0454 } 0455 else 0456 { 0457 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "API token received; " 0458 "querying user info."; 0459 0460 d->apiTokenExpires = (uint)(QDateTime::currentMSecsSinceEpoch() / 1000 + 0461 INAT_API_TOKEN_EXPIRATION); 0462 userInfo(cookies); 0463 } 0464 } 0465 0466 // ------------------------------------------------------------------------------------------ 0467 0468 /** 0469 * Get login, name, and icon of authorized user. 0470 */ 0471 class Q_DECL_HIDDEN UserRequest : public Request 0472 { 0473 public: 0474 0475 explicit UserRequest(const QList<QNetworkCookie>& cookies) 0476 : m_cookies(cookies) 0477 { 0478 } 0479 0480 void reportError(INatTalker& talker, QNetworkReply::NetworkError, 0481 const QString& errorString) const override 0482 { 0483 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "users/me error" << errorString 0484 << "after" << durationMilliSecs() << "msecs."; 0485 0486 Q_EMIT talker.signalLinkingFailed(QLatin1String("user-info request " 0487 "failed")); 0488 } 0489 0490 void parseResponse(INatTalker& talker, 0491 const QByteArray& data) const override 0492 { 0493 QJsonObject json = parseJsonResponse(data); 0494 0495 if (json.contains(RESULTS) && (json[RESULTS].toArray().count() == 1)) 0496 { 0497 QJsonObject result = json[RESULTS].toArray()[0].toObject(); 0498 QString username(result[LOGIN].toString()); 0499 Q_EMIT talker.signalLinkingSucceeded(username, 0500 result[NAME].toString(), 0501 QUrl(result[ICON].toString())); 0502 talker.d->store->setGroupKey(talker.d->serviceName + username); 0503 0504 // save api token 0505 0506 talker.d->store->setValue(talker.d->keyToken, talker.d->apiToken); 0507 0508 // save api token expiration 0509 0510 talker.d->store->setValue(talker.d->keyExpires, QString::number(talker.d->apiTokenExpires)); 0511 0512 // save cookies 0513 0514 QDateTime now(QDateTime::currentDateTime()); 0515 QByteArray saveCookies; 0516 0517 for (const auto& cookie : m_cookies) 0518 { 0519 if (!INatBrowserDlg::filterCookie(cookie, true, now)) 0520 { 0521 continue; 0522 } 0523 0524 if (!saveCookies.isEmpty()) 0525 { 0526 saveCookies += COOKIE_SEPARATOR; 0527 } 0528 0529 saveCookies += cookie.toRawForm(); 0530 } 0531 0532 talker.d->store->setValue(talker.d->keyCookies, 0533 QString::fromUtf8(saveCookies)); 0534 } 0535 else 0536 { 0537 Q_EMIT talker.signalLinkingFailed(QLatin1String("user-info request " 0538 "failed")); 0539 } 0540 0541 if (talker.m_authProgressDlg) 0542 { 0543 talker.m_authProgressDlg->setValue(2); 0544 talker.m_authProgressDlg->hide(); 0545 } 0546 0547 Q_EMIT talker.signalBusy(false); 0548 } 0549 0550 private: 0551 0552 QList<QNetworkCookie> m_cookies; 0553 }; 0554 0555 void INatTalker::userInfo(const QList<QNetworkCookie>& cookies) 0556 { 0557 if (d->apiToken.isEmpty()) 0558 { 0559 return; 0560 } 0561 0562 Q_EMIT signalBusy(true); 0563 0564 if (m_authProgressDlg) 0565 { 0566 m_authProgressDlg->setLabelText(QLatin1String("<font color=\"#74ac00\">") + 0567 i18n("iNaturalist") + 0568 QLatin1String("</font> ") + 0569 i18n("Login")); 0570 m_authProgressDlg->setMaximum(2); 0571 m_authProgressDlg->setValue(1); 0572 m_authProgressDlg->show(); 0573 } 0574 0575 QUrl url(d->apiUrl + QLatin1String("users/me")); 0576 QNetworkRequest netRequest(url); 0577 netRequest.setHeader(QNetworkRequest::ContentTypeHeader, 0578 QLatin1String(O2_MIME_TYPE_JSON)); 0579 netRequest.setRawHeader("Authorization", d->apiToken.toLatin1()); 0580 d->pendingRequests.insert(d->netMngr->get(netRequest), 0581 new UserRequest(cookies)); 0582 } 0583 0584 // ------------------------------------------------------------------------------------------ 0585 0586 /** 0587 * Load a URL and return result as QByteArry; used for images (icons). 0588 */ 0589 class Q_DECL_HIDDEN LoadUrlRequest : public Request 0590 { 0591 public: 0592 0593 LoadUrlRequest(const QUrl& url, int retries) 0594 : m_url (url), 0595 m_retries(retries) 0596 { 0597 } 0598 0599 void reportError(INatTalker& talker, QNetworkReply::NetworkError code, 0600 const QString& errorString) const override 0601 { 0602 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Url" << m_url << "error" << errorString 0603 << "after" << durationMilliSecs() << "msecs."; 0604 0605 if (networkErrorRetry(code) && (m_retries < MAX_RETRIES)) 0606 { 0607 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Attempting to load" << m_url << "again, retry" 0608 << m_retries + 1 << "of" << MAX_RETRIES; 0609 0610 talker.loadUrl(m_url, m_retries + 1); 0611 } 0612 else if (talker.d->cachedLoadUrls.contains(m_url)) 0613 { 0614 talker.d->cachedLoadUrls.remove(m_url); 0615 } 0616 } 0617 0618 void parseResponse(INatTalker& talker, 0619 const QByteArray& data) const override 0620 { 0621 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Url" << m_url << "loaded in" 0622 << durationMilliSecs() << "msecs."; 0623 0624 talker.d->cachedLoadUrls.insert(m_url, data); 0625 0626 Q_EMIT talker.signalLoadUrlSucceeded(m_url, data); 0627 } 0628 0629 private: 0630 0631 QUrl m_url; 0632 int m_retries; 0633 0634 private: 0635 0636 Q_DISABLE_COPY(LoadUrlRequest) 0637 }; 0638 0639 void INatTalker::loadUrl(const QUrl& imgUrl, int retries) 0640 { 0641 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Requesting url" << imgUrl.url(); 0642 0643 if (imgUrl.isEmpty() || imgUrl.isLocalFile() || imgUrl.isRelative()) 0644 { 0645 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Ignoring; NOT loading url" 0646 << imgUrl; 0647 return; 0648 } 0649 0650 if (d->cachedLoadUrls.contains(imgUrl)) 0651 { 0652 const QByteArray& result = d->cachedLoadUrls.value(imgUrl); 0653 0654 if (!result.isEmpty()) 0655 { 0656 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Url" << imgUrl 0657 << "found in cache."; 0658 0659 Q_EMIT signalLoadUrlSucceeded(imgUrl, result); 0660 } 0661 else 0662 { 0663 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Url load of" << imgUrl 0664 << "already in progress; ignoring request."; 0665 } 0666 0667 return; 0668 } 0669 0670 // empty cache entry means load in progress 0671 0672 d->cachedLoadUrls.insert(imgUrl, QByteArray()); 0673 0674 QNetworkRequest netRequest(imgUrl); 0675 d->pendingRequests.insert(d->netMngr->get(netRequest), 0676 new LoadUrlRequest(imgUrl, retries)); 0677 } 0678 0679 // ------------------------------------------------------------------------------------------ 0680 0681 /** 0682 * taxon auto-completion 0683 */ 0684 class Q_DECL_HIDDEN AutoCompletionRequest : public Request 0685 { 0686 public: 0687 0688 explicit AutoCompletionRequest(const QString& name) 0689 : m_partialName(name) 0690 { 0691 } 0692 0693 void reportError(INatTalker&, QNetworkReply::NetworkError, 0694 const QString& errorString) const override 0695 { 0696 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Taxon auto-completion" << m_partialName 0697 << "error" << errorString 0698 << "after" << durationMilliSecs() << "msecs."; 0699 } 0700 0701 void parseResponse(INatTalker& talker, 0702 const QByteArray& data) const override 0703 { 0704 QJsonObject json = parseJsonResponse(data); 0705 0706 if (json.contains(RESULTS)) 0707 { 0708 QJsonArray results = json[RESULTS].toArray(); 0709 QList<Taxon> taxa; 0710 0711 for (const auto& result : results) 0712 { 0713 taxa << parseTaxon(result.toObject()); 0714 } 0715 0716 QPair<QString, QList<Taxon> > result(m_partialName, taxa); 0717 talker.d->cachedAutoCompletions.insert(m_partialName, result); 0718 0719 Q_EMIT talker.signalTaxonAutoCompletions(result); 0720 } 0721 } 0722 0723 private: 0724 0725 QString m_partialName; 0726 0727 private: 0728 0729 Q_DISABLE_COPY(AutoCompletionRequest) 0730 }; 0731 0732 void INatTalker::taxonAutoCompletions(const QString& partialName) 0733 { 0734 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Requesting taxon auto-completions for" 0735 << partialName; 0736 0737 if (d->cachedAutoCompletions.contains(partialName)) 0738 { 0739 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Taxon auto-completions for" 0740 << partialName << "found in cache."; 0741 0742 Q_EMIT signalTaxonAutoCompletions(d->cachedAutoCompletions. 0743 value(partialName)); 0744 return; 0745 } 0746 0747 QUrl url(d->apiUrl + QLatin1String("taxa/autocomplete")); 0748 QUrlQuery query; 0749 query.addQueryItem(QLatin1String("q"), partialName); 0750 query.addQueryItem(QLatin1String("is_active"), QLatin1String("true")); 0751 query.addQueryItem(PER_PAGE, QString::number(12)); 0752 query.addQueryItem(LOCALE, locale.name()); 0753 url.setQuery(query.query()); 0754 0755 QNetworkRequest netRequest(url); 0756 netRequest.setHeader(QNetworkRequest::ContentTypeHeader, 0757 QLatin1String(O2_MIME_TYPE_JSON)); 0758 0759 d->pendingRequests.insert(d->netMngr->get(netRequest), 0760 new AutoCompletionRequest(partialName)); 0761 } 0762 0763 // ------------------------------------------------------------------------------------------ 0764 0765 /** 0766 * get nearby places 0767 */ 0768 class Q_DECL_HIDDEN NearbyPlacesRequest : public Request 0769 { 0770 0771 public: 0772 0773 NearbyPlacesRequest(double latitude, double longitude, const QString& query) 0774 : m_latitude (latitude), 0775 m_longitude(longitude), 0776 m_query (query) 0777 { 0778 Q_UNUSED(m_latitude); 0779 Q_UNUSED(m_longitude); 0780 } 0781 0782 void parseResponse(INatTalker& talker, 0783 const QByteArray& data) const override 0784 { 0785 QJsonObject json = parseJsonResponse(data); 0786 0787 if (json.contains(RESULTS)) 0788 { 0789 static const QString BBOX_AREA = QLatin1String("bbox_area"); 0790 static const QString DISPLAY_NAME = QLatin1String("display_name"); 0791 QJsonObject results = json[RESULTS].toObject(); 0792 QList<Place> places; 0793 0794 for (const auto& key : results.keys()) 0795 { 0796 for (const auto& placeValue : results.value(key).toArray()) 0797 { 0798 QJsonObject place = placeValue.toObject(); 0799 places.push_front(Place(place[DISPLAY_NAME].toString(), 0800 place[BBOX_AREA].toDouble())); 0801 } 0802 } 0803 0804 std::sort(places.begin(), places.end()); 0805 QStringList placesStrList; 0806 0807 for (const auto& place : places) 0808 { 0809 placesStrList << place.m_name; 0810 } 0811 0812 talker.d->cachedNearbyPlaces.insert(m_query, placesStrList); 0813 0814 Q_EMIT talker.signalNearbyPlaces(placesStrList); 0815 } 0816 } 0817 0818 void reportError(INatTalker&, QNetworkReply::NetworkError, 0819 const QString& errorString) const override 0820 { 0821 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Nearby places error" << errorString 0822 << "after" << durationMilliSecs() << "msecs."; 0823 } 0824 0825 private: 0826 0827 struct Place 0828 { 0829 QString m_name; 0830 double m_bboxArea; 0831 0832 Place() 0833 : m_bboxArea(0.0) 0834 { 0835 } 0836 0837 Place(const QString& n, double ba) 0838 : m_name (n), 0839 m_bboxArea(ba) 0840 { 0841 } 0842 0843 bool operator <(const Place& other) const 0844 { 0845 return (m_bboxArea < other.m_bboxArea); 0846 } 0847 }; 0848 0849 private: 0850 0851 double m_latitude; 0852 double m_longitude; 0853 QString m_query; 0854 0855 private: 0856 0857 Q_DISABLE_COPY(NearbyPlacesRequest) 0858 }; 0859 0860 void INatTalker::nearbyPlaces(double latitude, double longitude) 0861 { 0862 QUrl url(d->apiUrl + QLatin1String("places/nearby")); 0863 0864 QString lat = QString::number(latitude, 'f', GEOLOCATION_PRECISION); 0865 QString lng = QString::number(longitude, 'f', GEOLOCATION_PRECISION); 0866 0867 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Requesting nearby places for lat" 0868 << lat << "lon" << lng; 0869 QUrlQuery query; 0870 query.addQueryItem(QLatin1String("nelat"), lat); 0871 query.addQueryItem(QLatin1String("nelng"), lng); 0872 query.addQueryItem(QLatin1String("swlat"), lat); 0873 query.addQueryItem(QLatin1String("swlng"), lng); 0874 query.addQueryItem(PER_PAGE, QString::number(100)); 0875 url.setQuery(query.query()); 0876 0877 if (d->cachedNearbyPlaces.contains(query.query())) 0878 { 0879 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Nearby places for lat" << lat 0880 << "lon" << lng << "found in cache."; 0881 0882 Q_EMIT signalNearbyPlaces(d->cachedNearbyPlaces.value(query.query())); 0883 0884 return; 0885 } 0886 0887 QNetworkRequest netRequest(url); 0888 netRequest.setHeader(QNetworkRequest::ContentTypeHeader, 0889 QLatin1String(O2_MIME_TYPE_JSON)); 0890 0891 d->pendingRequests.insert(d->netMngr->get(netRequest), 0892 new NearbyPlacesRequest(latitude, longitude, 0893 query.query())); 0894 } 0895 0896 // ------------------------------------------------------------------------------------------ 0897 0898 /** 0899 * Find the closest observation; used as a sanity check for identifications: 0900 * when the closest known observation is hundreds of kilometers away, we 0901 * have likely misidentified the organism in our photo. 0902 */ 0903 class Q_DECL_HIDDEN NearbyObservationRequest : public Request 0904 { 0905 0906 public: 0907 0908 NearbyObservationRequest(uint taxon, double latitude, double longitude, 0909 double km, const QString& query) 0910 : m_taxon (taxon), 0911 m_latitude (latitude), 0912 m_longitude(longitude), 0913 m_radiusKm (km), 0914 m_query (query) 0915 { 0916 } 0917 0918 void parseResponse(INatTalker& talker, 0919 const QByteArray& data) const override 0920 { 0921 QJsonObject json = parseJsonResponse(data); 0922 0923 if (json.contains(TOTAL_RESULTS)) 0924 { 0925 static const double MAX_DISTANGE_KM = EARTH_RADIUS_KM * M_PI; 0926 int total_results = json[TOTAL_RESULTS].toInt(); 0927 0928 if (total_results == 0) 0929 { 0930 if (m_radiusKm < MAX_DISTANGE_KM) 0931 { 0932 // Try again, double the radius. 0933 0934 talker.closestObservation(m_taxon, m_latitude, m_longitude, 0935 2 * m_radiusKm, m_query); 0936 } 0937 else 0938 { 0939 // The entire Earth searched, no observation found. 0940 0941 talker.d->cachedNearbyObservations.insert(m_query, 0942 INatTalker::NearbyObservation()); 0943 0944 Q_EMIT talker.signalNearbyObservation(INatTalker:: 0945 NearbyObservation()); 0946 } 0947 } 0948 else 0949 { 0950 INatTalker::NearbyObservation closestObscured 0951 ( 0952 -1, 0953 0.0, 0954 0.0, 0955 MAX_DISTANGE_KM * 1000.0, 0956 true, 0957 m_taxon, 0958 m_latitude, 0959 m_longitude 0960 ); 0961 0962 INatTalker::NearbyObservation closestOpen 0963 ( 0964 -1, 0965 0.0, 0966 0.0, 0967 MAX_DISTANGE_KM * 1000.0, 0968 false, 0969 m_taxon, 0970 m_latitude, 0971 m_longitude 0972 ); 0973 0974 QJsonArray results = json[RESULTS].toArray(); 0975 0976 for (const auto& resultValue : results) 0977 { 0978 QJsonObject result = resultValue.toObject(); 0979 0980 if (!result.contains(GEOJSON)) 0981 { 0982 continue; 0983 } 0984 0985 int observationId = result[ID].toInt(); 0986 QJsonArray coordinates(result[GEOJSON].toObject() 0987 [COORDINATES].toArray()); 0988 double latitude = coordinates[1].toDouble(); 0989 double longitude = coordinates[0].toDouble(); 0990 bool obscured = result[OBSCURED].toBool(); 0991 double distanceMeters = distanceBetween(m_latitude, 0992 m_longitude, 0993 latitude, 0994 longitude); 0995 0996 if (obscured) 0997 { 0998 if (distanceMeters < closestObscured.m_distanceMeters) 0999 { 1000 closestObscured.updateObservation(observationId, 1001 latitude, 1002 longitude, 1003 distanceMeters); 1004 } 1005 } 1006 else if (distanceMeters < closestOpen.m_distanceMeters) 1007 { 1008 closestOpen.updateObservation(observationId, 1009 latitude, longitude, 1010 distanceMeters); 1011 } 1012 } 1013 1014 double newDistanceMeters = closestOpen.isValid() ? closestOpen.m_distanceMeters 1015 : closestObscured.m_distanceMeters; 1016 1017 if ((results.count() < total_results) && (newDistanceMeters > 10.0)) 1018 { 1019 // There are additional observations that we have not 1020 // downloaded; request closer ones. 1021 1022 talker.closestObservation(m_taxon, m_latitude, m_longitude, 1023 newDistanceMeters / 999.0, 1024 m_query); 1025 } 1026 else 1027 { 1028 talker.d->cachedNearbyObservations.insert(m_query, 1029 closestOpen.isValid() ? closestOpen 1030 : closestObscured); 1031 1032 Q_EMIT talker.signalNearbyObservation(closestOpen.isValid() 1033 ? closestOpen 1034 : closestObscured); 1035 } 1036 } 1037 } 1038 } 1039 1040 void reportError(INatTalker&, QNetworkReply::NetworkError, 1041 const QString& errorString) const override 1042 { 1043 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Closest observation error" << errorString 1044 << "after" << durationMilliSecs() << "msecs."; 1045 } 1046 1047 private: 1048 1049 uint m_taxon; 1050 double m_latitude; 1051 double m_longitude; 1052 double m_radiusKm; 1053 QString m_query; 1054 1055 private: 1056 1057 Q_DISABLE_COPY(NearbyObservationRequest) 1058 }; 1059 1060 void INatTalker::closestObservation(uint taxon, double latitude, 1061 double longitude, double radiusKm, 1062 const QString& origQuery) 1063 { 1064 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Requesting closest observation of" 1065 << taxon << "to" << latitude << longitude 1066 << "with radius" << radiusKm << "km."; 1067 1068 QUrl url(d->apiUrl + OBSERVATIONS); 1069 1070 QUrlQuery query; 1071 query.addQueryItem(QLatin1String("geo"), QLatin1String("true")); 1072 query.addQueryItem(TAXON_ID, QString::number(taxon)); 1073 query.addQueryItem(QLatin1String("lat"), QString::number(latitude, 'f', 1074 GEOLOCATION_PRECISION)); 1075 query.addQueryItem(QLatin1String("lng"), QString::number(longitude, 'f', 1076 GEOLOCATION_PRECISION)); 1077 query.addQueryItem(QLatin1String("radius"), QString::number(radiusKm, 'f', 1078 RADIUS_PRECISION)); 1079 query.addQueryItem(QLatin1String("quality_grade"), 1080 QLatin1String("research")); 1081 query.addQueryItem(LOCALE, locale.name()); 1082 query.addQueryItem(PER_PAGE, QString::number(100)); 1083 url.setQuery(query.query()); 1084 1085 if (d->cachedNearbyObservations.contains(query.query())) 1086 { 1087 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Closest observation of" 1088 << taxon << "at" << latitude 1089 << longitude << "with radius" 1090 << radiusKm << "km found in cache."; 1091 1092 Q_EMIT signalNearbyObservation(d->cachedNearbyObservations.value( 1093 query.query())); 1094 return; 1095 } 1096 1097 QNetworkRequest netRequest(url); 1098 netRequest.setHeader(QNetworkRequest::ContentTypeHeader, 1099 QLatin1String(O2_MIME_TYPE_JSON)); 1100 1101 d->pendingRequests.insert(d->netMngr->get(netRequest), 1102 new NearbyObservationRequest(taxon, latitude, longitude, radiusKm, 1103 origQuery.isEmpty() ? query.query() 1104 : origQuery)); 1105 } 1106 1107 // ------------------------------------------------------------------------------------------ 1108 1109 /** 1110 * get taxon suggestions for an image 1111 */ 1112 class Q_DECL_HIDDEN ComputerVisionRequest : public Request 1113 { 1114 1115 public: 1116 1117 ComputerVisionRequest(const QString& imgPath, const QString& tmpFile) 1118 : m_imagePath (imgPath), 1119 m_tmpFilePath(tmpFile) 1120 { 1121 } 1122 1123 ~ComputerVisionRequest() 1124 { 1125 if (!m_tmpFilePath.isEmpty() && QFile::exists(m_tmpFilePath)) 1126 { 1127 QFile::remove(m_tmpFilePath); 1128 } 1129 } 1130 1131 void parseScore(const QJsonObject& json, QList<ComputerVisionScore>& scores) const 1132 { 1133 static const QString FREQUENCY_SCORE = QLatin1String("frequency_score"); 1134 static const QString VISION_SCORE = QLatin1String("vision_score"); 1135 static const QString COMBINED_SCORE = QLatin1String("combined_score"); 1136 1137 double frequency_score = 0.0; 1138 double vision_score = 0.0; 1139 double combined_score = 0.0; 1140 Taxon taxon; 1141 1142 if (json.contains(FREQUENCY_SCORE)) 1143 { 1144 frequency_score = json[FREQUENCY_SCORE].toDouble(); 1145 } 1146 1147 if (json.contains(VISION_SCORE)) 1148 { 1149 vision_score = json[VISION_SCORE].toDouble(); 1150 } 1151 1152 if (json.contains(COMBINED_SCORE)) 1153 { 1154 combined_score = json[COMBINED_SCORE].toDouble(); 1155 } 1156 1157 if (json.contains(TAXON)) 1158 { 1159 taxon = parseTaxon(json[TAXON].toObject()); 1160 } 1161 1162 scores << ComputerVisionScore(frequency_score, vision_score, 1163 combined_score, taxon); 1164 } 1165 1166 void parseResponse(INatTalker& talker, 1167 const QByteArray& data) const override 1168 { 1169 static const QString COMMON_ANCESTOR = QLatin1String("common_ancestor"); 1170 1171 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Computer vision for" << m_imagePath 1172 << "took" << durationMilliSecs() << "msecs."; 1173 1174 QJsonObject json = parseJsonResponse(data); 1175 QList<ComputerVisionScore> scores; 1176 1177 if (json.contains(COMMON_ANCESTOR)) 1178 { 1179 parseScore(json[COMMON_ANCESTOR].toObject(), scores); 1180 } 1181 1182 if (json.contains(RESULTS)) 1183 { 1184 for (const auto& result : json[RESULTS].toArray()) 1185 { 1186 parseScore(result.toObject(), scores); 1187 } 1188 } 1189 1190 ImageScores result(m_imagePath, scores); 1191 talker.d->cachedImageScores.insert(m_imagePath, result); 1192 Q_EMIT talker.signalComputerVisionResults(result); 1193 } 1194 1195 void reportError(INatTalker&, QNetworkReply::NetworkError, 1196 const QString& errorString) const override 1197 { 1198 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Computer vision error" << errorString 1199 << "after" << durationMilliSecs() << "msecs."; 1200 } 1201 1202 private: 1203 1204 QString m_imagePath; 1205 QString m_tmpFilePath; 1206 1207 private: 1208 1209 Q_DISABLE_COPY(ComputerVisionRequest) 1210 }; 1211 1212 void INatTalker::computerVision(const QUrl& localImage) 1213 { 1214 if (localImage.isEmpty() || (apiTokenExpiresIn() <= 0)) 1215 { 1216 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Computer-vision API not called:" 1217 << (localImage.isEmpty() ? "No image." 1218 : "Not logged in."); 1219 return; 1220 } 1221 1222 enum 1223 { 1224 HEIGHT = 299, 1225 WIDTH = 299 1226 }; 1227 1228 QString path = localImage.toLocalFile(); 1229 1230 if (d->cachedImageScores.contains(path)) 1231 { 1232 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Vision identification for" 1233 << localImage.toLocalFile() 1234 << "found in cache."; 1235 1236 Q_EMIT signalComputerVisionResults(d->cachedImageScores.value(path)); 1237 1238 return; 1239 } 1240 1241 QImage image = PreviewLoadThread::loadHighQualitySynchronously(path). 1242 copyQImage(); 1243 1244 if (image.isNull()) 1245 { 1246 image.load(path); 1247 } 1248 1249 path = tmpFileName(path); 1250 image = image.scaled(WIDTH, HEIGHT); 1251 image.save(path, "JPEG"); 1252 1253 QList<Parameter> parameters; 1254 1255 DItemInfo info(d->iface->itemInfo(localImage)); 1256 1257 if (info.hasGeolocationInfo()) 1258 { 1259 static const QString lat = QLatin1String("lat"); 1260 parameters << Parameter(lat, QString::number(info.latitude(), 'f', 1261 GEOLOCATION_PRECISION)); 1262 static const QString lng = QLatin1String("lng"); 1263 parameters << Parameter(lng, QString::number(info.longitude(), 'f', 1264 GEOLOCATION_PRECISION)); 1265 } 1266 1267 QDateTime dateTime = info.dateTime(); 1268 1269 if (dateTime.isValid()) 1270 { 1271 parameters << Parameter(OBSERVED_ON, 1272 dateTime.date().toString(Qt::ISODate)); 1273 } 1274 1275 parameters << Parameter(LOCALE, locale.name()); 1276 1277 QHttpMultiPart* const multiPart = getMultiPart(parameters, 1278 QLatin1String("image"), 1279 QFileInfo(path).fileName(), 1280 path); 1281 1282 QUrl url(d->apiUrl + QLatin1String("computervision/score_image")); 1283 QNetworkRequest netRequest(url); 1284 netRequest.setRawHeader("Authorization", d->apiToken.toLatin1()); 1285 1286 QNetworkReply* const reply = d->netMngr->post(netRequest, multiPart); 1287 multiPart->setParent(reply); 1288 d->pendingRequests.insert(reply, new ComputerVisionRequest(localImage.toLocalFile(), path)); 1289 } 1290 1291 QString INatTalker::tmpFileName(const QString& path) 1292 { 1293 QString suffix; 1294 1295 for ( ; ; ) 1296 { 1297 QString tmpFn = WSToolUtils::makeTemporaryDir(d->serviceName.toLatin1().constData()). 1298 filePath(QFileInfo(path).baseName().trimmed() + suffix + QLatin1String(".jpg")); 1299 1300 if (!QFile::exists(tmpFn)) 1301 { 1302 return tmpFn; 1303 } 1304 1305 suffix += QLatin1String("_"); 1306 } 1307 } 1308 1309 // ------------------------------------------------------------------------------------------ 1310 1311 class Q_DECL_HIDDEN VerifyCreateObservationRequest : public Request 1312 { 1313 public: 1314 1315 VerifyCreateObservationRequest(const QByteArray& params, 1316 const INatTalker::PhotoUploadRequest& req, 1317 const QString& observed_on, 1318 int taxon_id, int retries) 1319 : m_parameters(params), 1320 m_uploadRequest(req), 1321 m_observed_on(observed_on), 1322 m_taxon_id(taxon_id), 1323 m_retries(retries) 1324 { 1325 } 1326 1327 void reportError(INatTalker& talker, QNetworkReply::NetworkError code, 1328 const QString& errorString) const override 1329 { 1330 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "VerifyCreateObservation: " << errorString 1331 << "after" << durationMilliSecs() << "msecs."; 1332 1333 if (networkErrorRetry(code) && (m_retries < MAX_RETRIES)) 1334 { 1335 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Attempting to call " 1336 "VerifyCreateObservation again, retry" 1337 << m_retries+1 << "of" << MAX_RETRIES; 1338 1339 talker.verifyCreateObservation(m_parameters, m_uploadRequest, 1, 1340 m_retries+1); 1341 } 1342 else 1343 { 1344 QMessageBox::critical(QApplication::activeWindow(), 1345 i18nc("@title:window", "ERROR While Creating Observation"), 1346 errorString); 1347 } 1348 } 1349 1350 void parseResponse(INatTalker& talker, 1351 const QByteArray& data) const override 1352 { 1353 QJsonObject json = parseJsonResponse(data); 1354 int observationId = -1; 1355 1356 if (json.contains(TOTAL_RESULTS) && 1357 json.contains(PER_PAGE) && 1358 json.contains(RESULTS) && 1359 json.contains(PAGE)) 1360 { 1361 int totalResults = json[TOTAL_RESULTS].toInt(); 1362 int perPage = json[PER_PAGE].toInt(); 1363 QJsonArray results = json[RESULTS].toArray(); 1364 1365 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Observation check:" << results.count() 1366 << "results of" << totalResults 1367 << "received in" << durationMilliSecs() << "msecs."; 1368 1369 for (int i = 0 ; i < results.count() ; ++i) 1370 { 1371 QJsonObject result = results[i].toObject(); 1372 1373 if (result.contains(OBSERVED_ON_STRING) && 1374 result.contains(TAXON) && 1375 result[OBSERVED_ON_STRING].toString() == m_observed_on && 1376 result[TAXON].toObject()[ID].toInt() == m_taxon_id) 1377 { 1378 observationId = result[ID].toInt(); 1379 break; 1380 } 1381 } 1382 1383 // Not found. If the server has more results request them. 1384 1385 if ((observationId < 0) && (results.count() >= perPage)) 1386 { 1387 talker.verifyCreateObservation(m_parameters, m_uploadRequest, 1388 json[PAGE].toInt()+1, m_retries); 1389 return; 1390 } 1391 } 1392 1393 if (observationId > 0) 1394 { 1395 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "VerifyCreateObservation: " 1396 "observation" << observationId 1397 << "of taxon_id" << m_taxon_id 1398 << "of" << m_observed_on 1399 << "found; uploading photos."; 1400 1401 INatTalker::PhotoUploadRequest request(m_uploadRequest); 1402 request.m_observationId = observationId; 1403 1404 Q_EMIT talker.signalObservationCreated(request); 1405 } 1406 else 1407 { 1408 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "VerifyCreateObservation: " 1409 "observation of taxon_id" 1410 << m_taxon_id << "at" << m_observed_on 1411 << "not found on server; uploading again."; 1412 1413 talker.createObservation(m_parameters, m_uploadRequest); 1414 } 1415 } 1416 1417 private: 1418 1419 QByteArray m_parameters; 1420 INatTalker::PhotoUploadRequest m_uploadRequest; 1421 QString m_observed_on; 1422 int m_taxon_id; 1423 int m_retries; 1424 1425 private: 1426 1427 Q_DISABLE_COPY(VerifyCreateObservationRequest) 1428 }; 1429 1430 void INatTalker::verifyCreateObservation(const QByteArray& parameters, 1431 const PhotoUploadRequest& photoUpload, 1432 int page, int retries) 1433 { 1434 QJsonObject json = parseJsonResponse(parameters); 1435 1436 QUrl url(d->apiUrl + OBSERVATIONS); 1437 QUrlQuery query; 1438 query.addQueryItem(QLatin1String("user_login"), photoUpload.m_user); 1439 query.addQueryItem(QLatin1String("photos"), QLatin1String("false")); 1440 query.addQueryItem(PER_PAGE, QString::number(200)); 1441 query.addQueryItem(PAGE, QString::number(page)); 1442 1443 int taxon_id = 0; 1444 QString observed_on; 1445 1446 if (json.contains(OBSERVATION)) 1447 { 1448 QJsonObject parms = json[OBSERVATION].toObject(); 1449 1450 if (parms.contains(OBSERVED_ON_STRING)) 1451 { 1452 observed_on = parms[OBSERVED_ON_STRING].toString(); 1453 query.addQueryItem(OBSERVED_ON, observed_on.left(10)); 1454 } 1455 1456 if (parms.contains(TAXON_ID)) 1457 { 1458 taxon_id = parms[TAXON_ID].toInt(); 1459 query.addQueryItem(TAXON_ID, QString::number(taxon_id)); 1460 } 1461 } 1462 1463 url.setQuery(query.query()); 1464 1465 QNetworkRequest netRequest(url); 1466 netRequest.setHeader(QNetworkRequest::ContentTypeHeader, 1467 QLatin1String(O2_MIME_TYPE_JSON)); 1468 netRequest.setRawHeader("Authorization", d->apiToken.toLatin1()); 1469 1470 PhotoUploadRequest upload(photoUpload); 1471 upload.m_apiKey = d->apiToken; 1472 d->pendingRequests.insert(d->netMngr->get(netRequest), 1473 new VerifyCreateObservationRequest(parameters, 1474 upload, 1475 observed_on, 1476 taxon_id, 1477 retries)); 1478 } 1479 1480 // ------------------------------------------------------------------------------------------ 1481 1482 class Q_DECL_HIDDEN CreateObservationRequest : public Request 1483 { 1484 public: 1485 1486 CreateObservationRequest(const QByteArray& params, 1487 const INatTalker::PhotoUploadRequest& req) 1488 : m_parameters(params), 1489 m_uploadRequest(req) 1490 { 1491 } 1492 1493 void reportError(INatTalker& talker, QNetworkReply::NetworkError code, 1494 const QString& errorString) const override 1495 { 1496 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Observation not created due to " 1497 "network error" << errorString 1498 << "after" << durationMilliSecs() << "msecs."; 1499 1500 if (networkErrorRetry(code)) 1501 { 1502 talker.verifyCreateObservation(m_parameters, m_uploadRequest, 1, 0); 1503 } 1504 else 1505 { 1506 QMessageBox::critical(QApplication::activeWindow(), 1507 i18nc("@title:window", "ERROR While Creating Observation"), 1508 errorString); 1509 } 1510 } 1511 1512 void parseResponse(INatTalker& talker, 1513 const QByteArray& data) const override 1514 { 1515 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Observation created in" 1516 << durationMilliSecs() << "msecs."; 1517 1518 QJsonObject json = parseJsonResponse(data); 1519 1520 if (json.contains(ID)) 1521 { 1522 INatTalker::PhotoUploadRequest request(m_uploadRequest); 1523 request.m_observationId = json[ID].toInt(); 1524 1525 Q_EMIT talker.signalObservationCreated(request); 1526 } 1527 } 1528 1529 private: 1530 1531 QByteArray m_parameters; 1532 INatTalker::PhotoUploadRequest m_uploadRequest; 1533 1534 private: 1535 1536 Q_DISABLE_COPY(CreateObservationRequest) 1537 }; 1538 1539 void INatTalker::createObservation(const QByteArray& parameters, 1540 const PhotoUploadRequest& photoUpload) 1541 { 1542 QUrl url(d->apiUrl + OBSERVATIONS); 1543 1544 QNetworkRequest netRequest(url); 1545 netRequest.setHeader(QNetworkRequest::ContentTypeHeader, 1546 QLatin1String(O2_MIME_TYPE_JSON)); 1547 netRequest.setRawHeader("Authorization", d->apiToken.toLatin1()); 1548 1549 PhotoUploadRequest upload(photoUpload); 1550 upload.m_apiKey = d->apiToken; 1551 d->pendingRequests.insert(d->netMngr->post(netRequest, parameters), 1552 new CreateObservationRequest(parameters, upload)); 1553 } 1554 1555 // ------------------------------------------------------------------------------------------ 1556 1557 class Q_DECL_HIDDEN VerifyUploadPhotoRequest : public Request 1558 { 1559 public: 1560 1561 VerifyUploadPhotoRequest(const INatTalker::PhotoUploadRequest& req, 1562 int retries) 1563 : m_request(req), 1564 m_retries(retries) 1565 { 1566 } 1567 1568 void reportError(INatTalker& talker, QNetworkReply::NetworkError code, 1569 const QString& errorString) const override 1570 { 1571 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "VerifyPhotoUploadNextPhoto: " << errorString 1572 << "after" << durationMilliSecs() << "msecs."; 1573 1574 if (networkErrorRetry(code) && m_retries < MAX_RETRIES) 1575 { 1576 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Attempting to call " 1577 "VerifyPhotoUploadNextPhoto again, retry" 1578 << m_retries + 1 << "of" << MAX_RETRIES; 1579 1580 talker.verifyUploadNextPhoto(m_request, m_retries+1); 1581 } 1582 else 1583 { 1584 QMessageBox::critical(QApplication::activeWindow(), 1585 i18nc("@title:window", "ERROR While Uploading Photo"), 1586 errorString); 1587 } 1588 } 1589 1590 void parseResponse(INatTalker& talker, 1591 const QByteArray& data) const override 1592 { 1593 QJsonObject json = parseJsonResponse(data); 1594 1595 int noPhotos = 0; 1596 int lastObservationPhotoId = -1; 1597 int lastPhotoId = -1; 1598 1599 if (json.contains(TOTAL_RESULTS) && 1600 json.contains(RESULTS) && 1601 (json[TOTAL_RESULTS].toInt() == 1)) 1602 { 1603 QJsonObject result = json[RESULTS].toArray()[0].toObject(); 1604 1605 if (result.contains(OBSERVATION_PHOTOS)) 1606 { 1607 noPhotos = result[OBSERVATION_PHOTOS].toArray().count(); 1608 1609 if (noPhotos >= 1) 1610 { 1611 QJsonObject obsPhoto = result[OBSERVATION_PHOTOS]. 1612 toArray()[noPhotos-1].toObject(); 1613 lastObservationPhotoId = obsPhoto[ID].toInt(); 1614 lastPhotoId = obsPhoto[PHOTO].toObject()[ID].toInt(); 1615 } 1616 } 1617 1618 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "VerifyUploadNextPhoto:" << noPhotos 1619 << "photos on server," << m_request.m_images.count() 1620 << "photos to upload," << m_request.m_totalImages 1621 << "total photos, checked in" << durationMilliSecs() << "msecs."; 1622 1623 if ((noPhotos + m_request.m_images.count()) == 1624 m_request.m_totalImages) 1625 { 1626 talker.uploadNextPhoto(m_request); 1627 } 1628 else if ((noPhotos + m_request.m_images.count()) == 1629 (m_request.m_totalImages + 1)) 1630 { 1631 INatTalker::PhotoUploadResult uploadResult(m_request, 1632 lastObservationPhotoId, 1633 lastPhotoId); 1634 Q_EMIT talker.signalPhotoUploaded(uploadResult); 1635 } 1636 } 1637 else 1638 { 1639 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "VerifyPhotoUploadNextPhoto: " 1640 "observation" << m_request.m_observationId 1641 << "NOT FOUND in" << durationMilliSecs() << "msecs."; 1642 } 1643 } 1644 1645 private: 1646 1647 INatTalker::PhotoUploadRequest m_request; 1648 int m_retries; 1649 1650 private: 1651 1652 Q_DISABLE_COPY(VerifyUploadPhotoRequest) 1653 }; 1654 1655 void INatTalker::verifyUploadNextPhoto(const PhotoUploadRequest& request, 1656 int retries) 1657 { 1658 QUrl url(d->apiUrl + QLatin1String("observations/") + 1659 QString::number(request.m_observationId)); 1660 QNetworkRequest netRequest(url); 1661 netRequest.setHeader(QNetworkRequest::ContentTypeHeader, 1662 QLatin1String(O2_MIME_TYPE_JSON)); 1663 netRequest.setRawHeader("Authorization", request.m_apiKey.toLatin1()); 1664 d->pendingRequests.insert(d->netMngr->get(netRequest), 1665 new VerifyUploadPhotoRequest(request, retries)); 1666 } 1667 1668 // ------------------------------------------------------------------------------------------ 1669 1670 class Q_DECL_HIDDEN UploadPhotoRequest : public Request 1671 { 1672 public: 1673 1674 UploadPhotoRequest(const INatTalker::PhotoUploadRequest& req, 1675 const QString& tmpImg) 1676 : m_request (req), 1677 m_tmpImage(tmpImg) 1678 { 1679 } 1680 1681 ~UploadPhotoRequest() 1682 { 1683 if (!m_tmpImage.isEmpty() && QFile::exists(m_tmpImage)) 1684 { 1685 QFile::remove(m_tmpImage); 1686 } 1687 } 1688 1689 void reportError(INatTalker& talker, QNetworkReply::NetworkError code, 1690 const QString& errorString) const override 1691 { 1692 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Photo not uploaded due to " 1693 "network error" << errorString 1694 << "after" << durationMilliSecs() << "msecs."; 1695 1696 if (networkErrorRetry(code)) 1697 { 1698 talker.verifyUploadNextPhoto(m_request, 0); 1699 } 1700 else 1701 { 1702 QMessageBox::critical(QApplication::activeWindow(), 1703 i18nc("@title:window", "ERROR While Uploading Photo"), 1704 errorString); 1705 } 1706 } 1707 1708 void parseResponse(INatTalker& talker, 1709 const QByteArray& data) const override 1710 { 1711 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Photo" << m_request.m_images.front().toLocalFile() 1712 << "to observation" << m_request.m_observationId 1713 << "uploaded in" << durationMilliSecs() << "msecs."; 1714 1715 static const QString PHOTO_ID = QLatin1String("photo_id"); 1716 QJsonObject json = parseJsonResponse(data); 1717 1718 if (json.contains(PHOTO_ID)) 1719 { 1720 INatTalker::PhotoUploadResult result(m_request, json[ID].toInt(), 1721 json[PHOTO_ID].toInt()); 1722 Q_EMIT talker.signalPhotoUploaded(result); 1723 } 1724 } 1725 1726 private: 1727 1728 INatTalker::PhotoUploadRequest m_request; 1729 QString m_tmpImage; 1730 1731 private: 1732 1733 Q_DISABLE_COPY(UploadPhotoRequest) 1734 }; 1735 1736 void INatTalker::uploadNextPhoto(const PhotoUploadRequest& request) 1737 { 1738 QList<Parameter> parameters; 1739 parameters << Parameter(QLatin1String("observation_photo[observation_id]"), 1740 QString::number(request.m_observationId)); 1741 QString tmpImage; 1742 QString path = request.m_images.front().toLocalFile(); 1743 1744 bool isJpeg = (path.endsWith(QLatin1String(".jpg"), Qt::CaseInsensitive) || 1745 path.endsWith(QLatin1String(".jpeg"), Qt::CaseInsensitive)); 1746 1747 if (request.m_rescale || !isJpeg) 1748 { 1749 QImage image = PreviewLoadThread::loadHighQualitySynchronously(path). 1750 copyQImage(); 1751 1752 if (image.isNull()) 1753 { 1754 image.load(path); 1755 } 1756 1757 if (!image.isNull()) 1758 { 1759 tmpImage = tmpFileName(path); 1760 1761 if ((image.width() > request.m_maxDim) || 1762 (image.height() > request.m_maxDim)) 1763 { 1764 image = image.scaled(request.m_maxDim, request.m_maxDim, 1765 Qt::KeepAspectRatio, 1766 Qt::SmoothTransformation); 1767 } 1768 1769 image.save(tmpImage, "JPEG", request.m_quality); 1770 1771 if (!isJpeg) 1772 { 1773 path += QLatin1String(".jpeg"); 1774 } 1775 } 1776 } 1777 1778 QHttpMultiPart* const multiPart = getMultiPart(parameters, 1779 QLatin1String("file"), 1780 QFileInfo(path).fileName(), 1781 tmpImage.isEmpty() ? path 1782 : tmpImage); 1783 QUrl url(d->apiUrl + OBSERVATION_PHOTOS); 1784 QNetworkRequest netRequest(url); 1785 netRequest.setRawHeader("Authorization", request.m_apiKey.toLatin1()); 1786 QNetworkReply* const reply = d->netMngr->post(netRequest, multiPart); 1787 multiPart->setParent(reply); 1788 d->pendingRequests.insert(reply, new UploadPhotoRequest(request, tmpImage)); 1789 } 1790 1791 // ------------------------------------------------------------------------------------------ 1792 1793 class Q_DECL_HIDDEN DeleteObservationRequest : public Request 1794 { 1795 public: 1796 1797 DeleteObservationRequest(const QString& apiKey, int id, int retries) 1798 : m_apiKey (apiKey), 1799 m_observationId(id), 1800 m_retries (retries) 1801 { 1802 } 1803 1804 void reportError(INatTalker& talker, QNetworkReply::NetworkError code, 1805 const QString& errorString) const override 1806 { 1807 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Delete observation failed with " 1808 "error" << errorString 1809 << "after" << durationMilliSecs() << "msecs."; 1810 1811 if (networkErrorRetry(code) && m_retries < MAX_RETRIES) 1812 { 1813 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Attempting to delete " 1814 "observation" << m_observationId 1815 << "again, retry" << m_retries + 1 1816 << "of" << MAX_RETRIES; 1817 1818 talker.deleteObservation(m_observationId, m_apiKey, m_retries + 1); 1819 } 1820 else 1821 { 1822 QMessageBox::critical(QApplication::activeWindow(), 1823 i18nc("@title:window", "ERROR While Deleting Observation"), 1824 errorString); 1825 } 1826 } 1827 1828 void parseResponse(INatTalker& talker, const QByteArray&) const override 1829 { 1830 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Observation" << m_observationId 1831 << "deleted in" << durationMilliSecs() << "msecs."; 1832 1833 Q_EMIT talker.signalObservationDeleted(m_observationId); 1834 } 1835 1836 private: 1837 1838 QString m_apiKey; 1839 int m_observationId; 1840 int m_retries; 1841 1842 private: 1843 1844 Q_DISABLE_COPY(DeleteObservationRequest) 1845 }; 1846 1847 /** 1848 * Delete an observation; called when canceling uploads. 1849 */ 1850 void INatTalker::deleteObservation(int id, const QString& apiKey, int retries) 1851 { 1852 QUrl url(d->apiUrl + QLatin1String("observations/") + QString::number(id)); 1853 QNetworkRequest netRequest(url); 1854 netRequest.setHeader(QNetworkRequest::ContentTypeHeader, 1855 QLatin1String(O2_MIME_TYPE_JSON)); 1856 netRequest.setRawHeader("Authorization", apiKey.toLatin1()); 1857 d->pendingRequests.insert(d->netMngr->deleteResource(netRequest), 1858 new DeleteObservationRequest(apiKey, id, 1859 retries)); 1860 } 1861 1862 void INatTalker::cancel() 1863 { 1864 if (m_authProgressDlg && !m_authProgressDlg->isHidden()) 1865 { 1866 m_authProgressDlg->hide(); 1867 } 1868 1869 d->clear(); 1870 Q_EMIT signalBusy(false); 1871 } 1872 1873 void INatTalker::slotFinished(QNetworkReply* reply) 1874 { 1875 // ignore unexpected response 1876 1877 if (!d->pendingRequests.contains(reply)) 1878 { 1879 return; 1880 } 1881 1882 Request* const request = d->pendingRequests.take(reply); 1883 1884 if (reply->error() == QNetworkReply::NoError) 1885 { 1886 request->parseResponse(*this, reply->readAll()); 1887 } 1888 else 1889 { 1890 request->reportError(*this, reply->error(), reply->errorString()); 1891 } 1892 1893 delete request; 1894 reply->deleteLater(); 1895 } 1896 1897 void INatTalker::slotTimeout() 1898 { 1899 QList<QPair<QNetworkReply*, Request*> > timeoutList; 1900 QHash<QNetworkReply*, Request*>::const_iterator it; 1901 1902 for (it = d->pendingRequests.constBegin() ; 1903 it != d->pendingRequests.constEnd() ; ++it) 1904 { 1905 Request* const request = it.value(); 1906 1907 if (request->isTimeout()) 1908 { 1909 timeoutList.append(qMakePair(it.key(), request)); 1910 } 1911 } 1912 1913 for (const auto& pair : timeoutList) 1914 { 1915 QNetworkReply* const reply = pair.first; 1916 d->pendingRequests.remove(reply); 1917 1918 QNetworkReply::NetworkError errorCode = reply->error(); 1919 QString errorString = reply->errorString(); 1920 1921 reply->abort(); 1922 delete reply; 1923 1924 if (errorCode == QNetworkReply::NoError) 1925 { 1926 errorCode = QNetworkReply::TimeoutError; 1927 errorString = i18n("Timeout after exceeding %1 seconds", 1928 RESPONSE_TIMEOUT_SECS); 1929 } 1930 1931 Request* const request = pair.second; 1932 request->reportError(*this, errorCode, errorString); 1933 delete request; 1934 } 1935 } 1936 1937 } // namespace DigikamGenericINatPlugin 1938 1939 #include "moc_inattalker.cpp"