File indexing completed on 2025-01-19 03:53:05
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 by Joerg Lohse <joergmlpts at gmail dot com> 0010 * SPDX-FileCopyrightText: 2005-2008 by Vardhman Jain <vardhman at gmail dot com> 0011 * SPDX-FileCopyrightText: 2008-2024 by Gilles Caulier <caulier dot gilles at gmail dot com> 0012 * SPDX-FileCopyrightText: 2009 by Luka Renko <lure at kubuntu dot org> 0013 * 0014 * SPDX-License-Identifier: GPL-2.0-or-later 0015 * 0016 * ============================================================ */ 0017 0018 #include "inatwindow.h" 0019 0020 // Qt includes 0021 0022 #include <QPushButton> 0023 #include <QProgressDialog> 0024 #include <QPicture> 0025 #include <QPixmap> 0026 #include <QCheckBox> 0027 #include <QStringList> 0028 #include <QSpinBox> 0029 #include <QPointer> 0030 #include <QApplication> 0031 #include <QMenu> 0032 #include <QMessageBox> 0033 #include <QWindow> 0034 #include <QCloseEvent> 0035 #include <QTimeZone> 0036 #include <QJsonValue> 0037 #include <QJsonObject> 0038 #include <QJsonDocument> 0039 0040 // KDE includes 0041 0042 #include <klocalizedstring.h> 0043 #include <ksharedconfig.h> 0044 #include <kconfiggroup.h> 0045 0046 // Local includes 0047 0048 #include "dprogresswdg.h" 0049 #include "ditemslist.h" 0050 #include "dmetadata.h" 0051 #include "dtextedit.h" 0052 #include "wsselectuserdlg.h" 0053 #include "digikam_debug.h" 0054 #include "previewloadthread.h" 0055 #include "inatwidget_p.h" 0056 #include "inatbrowserdlg.h" 0057 #include "inatutils.h" 0058 0059 namespace DigikamGenericINatPlugin 0060 { 0061 0062 /** 0063 * iNaturalist.org allows up to 20 photos for each observation and 0064 * scales down photos to 2048 pixels. 0065 */ 0066 enum 0067 { 0068 MAX_OBSERVATION_PHOTOS = 20, 0069 MAX_DIMENSION = 2048, 0070 MAX_EDITED_PLACES = 5 0071 }; 0072 0073 static const QString xmpNameSpaceURI = QLatin1String("https://inaturalist.org/ns/1.0/"); 0074 static const QString xmpNameSpacePrefix = QLatin1String("iNaturalist"); 0075 0076 0077 class Q_DECL_HIDDEN INatWindow::Private 0078 { 0079 public: 0080 0081 explicit Private() 0082 : changeUserButton (nullptr), 0083 accountIcon (nullptr), 0084 removeAccount (nullptr), 0085 resizeCheckBox (nullptr), 0086 dimensionSpinBox (nullptr), 0087 imageQualitySpinBox (nullptr), 0088 userNameDisplayLabel (nullptr), 0089 authProgressDlg (nullptr), 0090 identificationImage (nullptr), 0091 identificationLabel (nullptr), 0092 identificationFromVision(false), 0093 closestKnownObservation (nullptr), 0094 observationDescription (nullptr), 0095 identificationEdit (nullptr), 0096 taxonPopup (nullptr), 0097 placesComboBox (nullptr), 0098 moreOptionsButton (nullptr), 0099 moreOptionsWidget (nullptr), 0100 photoMaxTimeDiffSpB (nullptr), 0101 photoMaxDistanceSpB (nullptr), 0102 closestObservationMaxSpB(nullptr), 0103 widget (nullptr), 0104 talker (nullptr), 0105 imglst (nullptr), 0106 latLonValid (false), 0107 latitude (0.0), 0108 longitude (0.0), 0109 inCancel (false), 0110 xmpNameSpace (false), 0111 selectUser (nullptr), 0112 iface (nullptr) 0113 { 0114 } 0115 0116 QString serviceName; 0117 0118 QPushButton* changeUserButton; 0119 QLabel* accountIcon; 0120 QPushButton* removeAccount; 0121 0122 QCheckBox* resizeCheckBox; 0123 0124 QSpinBox* dimensionSpinBox; 0125 QSpinBox* imageQualitySpinBox; 0126 0127 // account info 0128 0129 QString username; 0130 QString name; 0131 QUrl iconUrl; 0132 QTimer apiTokenExpiresTimer; 0133 QLabel* userNameDisplayLabel; 0134 QProgressDialog* authProgressDlg; 0135 0136 // identification 0137 0138 QLabel* identificationImage; 0139 QLabel* identificationLabel; 0140 bool identificationFromVision; 0141 QLabel* closestKnownObservation; 0142 DPlainTextEdit* observationDescription; 0143 TaxonEdit* identificationEdit; 0144 SuggestTaxonCompletion* taxonPopup; 0145 QComboBox* placesComboBox; 0146 0147 // additional options 0148 0149 QPushButton* moreOptionsButton; 0150 QWidget* moreOptionsWidget; 0151 QSpinBox* photoMaxTimeDiffSpB; 0152 QSpinBox* photoMaxDistanceSpB; 0153 QSpinBox* closestObservationMaxSpB; 0154 0155 INatWidget* widget; 0156 INatTalker* talker; 0157 0158 DItemsList* imglst; 0159 0160 // observation 0161 0162 Taxon identification; 0163 bool latLonValid; 0164 double latitude; 0165 double longitude; 0166 QDateTime observationDateTime; 0167 0168 QList<QString> editedPlaces; 0169 bool inCancel; 0170 bool xmpNameSpace; 0171 WSSelectUserDlg* selectUser; 0172 DInfoInterface* iface; 0173 }; 0174 0175 INatWindow::INatWindow(DInfoInterface* const iface, 0176 QWidget* const /*parent*/, 0177 const QString& serviceName) 0178 : WSToolDialog(nullptr, QString::fromLatin1("%1 Export Dialog"). 0179 arg(serviceName)), 0180 d(new Private) 0181 { 0182 d->iface = iface; 0183 d->serviceName = serviceName; 0184 setWindowTitle(i18nc("@title:window", "Export as %1 Observation", d->serviceName)); 0185 setModal(false); 0186 0187 KSharedConfigPtr config = KSharedConfig::openConfig(); 0188 KConfigGroup grp = config->group(QString::fromLatin1("%1 Export Settings"). 0189 arg(d->serviceName)); 0190 0191 if (grp.exists()) 0192 { 0193 qCDebug(DIGIKAM_WEBSERVICES_LOG) << QString::fromLatin1("%1 Export Settings").arg(d->serviceName) 0194 << "exists, deleting it."; 0195 grp.deleteGroup(); 0196 } 0197 0198 d->selectUser = new WSSelectUserDlg(nullptr, serviceName); 0199 d->widget = new INatWidget(this, iface, serviceName); 0200 0201 // Account 0202 0203 d->userNameDisplayLabel = d->widget->getUserNameLabel(); 0204 d->changeUserButton = d->widget->getChangeUserBtn(); 0205 d->removeAccount = d->widget->d->removeAccount; 0206 d->accountIcon = d->widget->d->accountIcon; 0207 0208 // Options 0209 0210 d->resizeCheckBox = d->widget->getResizeCheckBox(); 0211 d->dimensionSpinBox = d->widget->getDimensionSpB(); 0212 d->imageQualitySpinBox = d->widget->getImgQualitySpB(); 0213 0214 // Observation & identification 0215 0216 d->identificationImage = d->widget->d->identificationImage; 0217 d->identificationLabel = d->widget->d->identificationLabel; 0218 d->closestKnownObservation = d->widget->d->closestKnownObservation; 0219 d->observationDescription = d->widget->d->observationDescription; 0220 d->identificationEdit = d->widget->d->identificationEdit; 0221 d->taxonPopup = d->widget->d->taxonPopup; 0222 d->placesComboBox = d->widget->d->placesComboBox; 0223 d->moreOptionsButton = d->widget->d->moreOptionsButton; 0224 d->moreOptionsWidget = d->widget->d->moreOptionsWidget; 0225 d->photoMaxTimeDiffSpB = d->widget->d->photoMaxTimeDiffSpB; 0226 d->photoMaxDistanceSpB = d->widget->d->photoMaxDistanceSpB; 0227 d->closestObservationMaxSpB = d->widget->d->closestObservationMaxSpB; 0228 0229 // Image list 0230 0231 d->imglst = d->widget->d->imglst; 0232 0233 // max dimension supported by iNaturalist.org 0234 0235 d->dimensionSpinBox->setMaximum(MAX_DIMENSION); 0236 d->dimensionSpinBox->setValue(MAX_DIMENSION); 0237 0238 startButton()->setText(i18nc("@action:button", "Start Uploading")); 0239 startButton()->setToolTip(QString()); 0240 0241 setMainWidget(d->widget); 0242 d->widget->setMinimumSize(800, 600); 0243 0244 connect(d->imglst, SIGNAL(signalImageListChanged()), 0245 this, SLOT(slotImageListChanged())); 0246 0247 connect(d->photoMaxTimeDiffSpB, SIGNAL(valueChanged(int)), 0248 this, SLOT(slotValueChanged(int))); 0249 0250 connect(d->photoMaxDistanceSpB, SIGNAL(valueChanged(int)), 0251 this, SLOT(slotValueChanged(int))); 0252 0253 connect(d->closestObservationMaxSpB, SIGNAL(valueChanged(int)), 0254 this, SLOT(slotClosestChanged(int))); 0255 0256 connect(d->moreOptionsButton, SIGNAL(toggled(bool)), 0257 this, SLOT(slotMoreOptionsButton(bool))); 0258 0259 d->apiTokenExpiresTimer.setSingleShot(true); 0260 0261 connect(&d->apiTokenExpiresTimer, SIGNAL(timeout()), 0262 this, SLOT(slotApiTokenExpires())); 0263 0264 // ------------------------------------------------------------------------- 0265 0266 d->talker = new INatTalker(this, serviceName, d->iface); 0267 0268 d->taxonPopup->setTalker(d->talker); 0269 0270 connect(d->talker, SIGNAL(signalBusy(bool)), 0271 this, SLOT(slotBusy(bool))); 0272 0273 connect(d->talker, SIGNAL(signalLinkingSucceeded(QString,QString,QUrl)), 0274 this, SLOT(slotLinkingSucceeded(QString,QString,QUrl))); 0275 0276 connect(d->talker, SIGNAL(signalLinkingFailed(QString)), 0277 this, SLOT(slotLinkingFailed(QString))); 0278 0279 connect(d->talker, SIGNAL(signalLoadUrlSucceeded(QUrl,QByteArray)), 0280 this, SLOT(slotLoadUrlSucceeded(QUrl,QByteArray))); 0281 0282 connect(d->talker, SIGNAL(signalNearbyPlaces(QStringList)), 0283 this, SLOT(slotNearbyPlaces(QStringList))); 0284 0285 connect(d->talker, SIGNAL(signalNearbyObservation(INatTalker::NearbyObservation)), 0286 this, SLOT(slotNearbyObservation(INatTalker::NearbyObservation))); 0287 0288 connect(d->talker, SIGNAL(signalObservationCreated(INatTalker::PhotoUploadRequest)), 0289 this, SLOT(slotObservationCreated(INatTalker::PhotoUploadRequest))); 0290 0291 connect(d->talker, SIGNAL(signalPhotoUploaded(INatTalker::PhotoUploadResult)), 0292 this, SLOT(slotPhotoUploaded(INatTalker::PhotoUploadResult))); 0293 0294 connect(d->talker, SIGNAL(signalObservationDeleted(int)), 0295 this, SLOT(slotObservationDeleted(int))); 0296 0297 // ------------------------------------------------------------------------- 0298 0299 connect(d->changeUserButton, SIGNAL(clicked()), 0300 this, SLOT(slotUserChangeRequest())); 0301 0302 connect(d->removeAccount, SIGNAL(clicked()), 0303 this, SLOT(slotRemoveAccount())); 0304 0305 // ------------------------------------------------------------------------- 0306 0307 d->authProgressDlg = new QProgressDialog(this); 0308 d->authProgressDlg->setModal(true); 0309 d->authProgressDlg->setAutoReset(true); 0310 d->authProgressDlg->setAutoClose(true); 0311 d->authProgressDlg->setMaximum(0); 0312 d->authProgressDlg->reset(); 0313 0314 connect(d->authProgressDlg, SIGNAL(canceled()), 0315 this, SLOT(slotAuthCancel())); 0316 0317 d->talker->m_authProgressDlg = d->authProgressDlg; 0318 0319 // ------------------------------------------------------------------------- 0320 0321 connect(this, &QDialog::finished, 0322 this, &INatWindow::slotFinished); 0323 0324 connect(this, SIGNAL(cancelClicked()), 0325 this, SLOT(slotCancelClicked())); 0326 0327 connect(startButton(), &QPushButton::clicked, 0328 this, &INatWindow::slotUser1); 0329 0330 connect(d->taxonPopup, SIGNAL(signalTaxonSelected(Taxon,bool)), 0331 this, SLOT(slotTaxonSelected(Taxon,bool))); 0332 0333 connect(d->taxonPopup, SIGNAL(signalTaxonDeselected()), 0334 this, SLOT(slotTaxonDeselected())); 0335 0336 connect(d->taxonPopup, SIGNAL(signalComputerVision()), 0337 this, SLOT(slotComputerVision())); 0338 0339 d->selectUser->reactivate(); 0340 switchUser(); 0341 } 0342 0343 INatWindow::~INatWindow() 0344 { 0345 delete d->selectUser; 0346 delete d->authProgressDlg; 0347 delete d->talker; 0348 delete d->widget; 0349 0350 if (d->xmpNameSpace) 0351 { 0352 DMetadata::unregisterXmpNameSpace(xmpNameSpaceURI); 0353 } 0354 0355 delete d; 0356 } 0357 0358 void INatWindow::switchUser(bool restoreToken) 0359 { 0360 QString userName = d->username; 0361 QList<QNetworkCookie> cookies; 0362 0363 d->apiTokenExpiresTimer.stop(); 0364 d->talker->unLink(); 0365 d->username = QString(); 0366 d->name = QString(); 0367 d->iconUrl = QUrl(); 0368 d->widget->updateLabels(QString()); 0369 0370 // User gets to select a username unless the timer calls us because 0371 // our token has expired. 0372 0373 if (restoreToken) 0374 { 0375 userName = d->selectUser->getUserName(); 0376 } 0377 0378 // If we have a username, restore api token and cookies. 0379 0380 if (!userName.isEmpty() && 0381 d->talker->restoreApiToken(userName, cookies, restoreToken)) 0382 { 0383 // Done if api token has been restored, browser is not called anymore. 0384 0385 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Login skipped; restored api_token " 0386 "for user" << userName; 0387 return; 0388 } 0389 0390 // Pass cookies to browser; if "remember me" is checked on iNaturalist 0391 // website, the browser will re-login for 14 days without user interaction. 0392 0393 QPointer<INatBrowserDlg> dlg = new INatBrowserDlg(userName, cookies, this); 0394 0395 connect(dlg, SIGNAL(signalApiToken(QString,QList<QNetworkCookie>)), 0396 d->talker, SLOT(slotApiToken(QString,QList<QNetworkCookie>))); 0397 0398 (void)dlg->exec(); 0399 } 0400 0401 void INatWindow::slotApiTokenExpires() 0402 { 0403 switchUser(false); 0404 } 0405 0406 void INatWindow::setItemsList(const QList<QUrl>& urls) 0407 { 0408 d->widget->imagesList()->slotAddImages(urls); 0409 } 0410 0411 void INatWindow::closeEvent(QCloseEvent* e) 0412 { 0413 if (!e) 0414 { 0415 return; 0416 } 0417 0418 slotFinished(); 0419 e->accept(); 0420 } 0421 0422 void INatWindow::slotFinished() 0423 { 0424 writeSettings(); 0425 d->imglst->listView()->clear(); 0426 } 0427 0428 void INatWindow::setUiInProgressState(bool inProgress) 0429 { 0430 setRejectButtonMode(inProgress ? QDialogButtonBox::Cancel 0431 : QDialogButtonBox::Close); 0432 0433 if (inProgress) 0434 { 0435 d->widget->progressBar()->show(); 0436 } 0437 else 0438 { 0439 d->widget->progressBar()->hide(); 0440 d->widget->progressBar()->progressCompleted(); 0441 } 0442 } 0443 0444 void INatWindow::slotCancelClicked() 0445 { 0446 if (d->talker->stillUploading()) 0447 { 0448 d->inCancel = true; 0449 slotBusy(true); 0450 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Cancel clicked; deleting " 0451 "observation(s) being uploaded."; 0452 return; 0453 } 0454 0455 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Cancel clicked; not uploading."; 0456 0457 d->talker->cancel(); 0458 setUiInProgressState(false); 0459 } 0460 0461 void INatWindow::reactivate() 0462 { 0463 d->userNameDisplayLabel->setText(QString()); 0464 switchUser(); 0465 0466 d->widget->d->imglst->loadImagesFromCurrentSelection(); 0467 show(); 0468 } 0469 0470 static const char SETTING_RESIZE[] = "Resize"; 0471 static const char SETTING_DIM[] = "Maximum Width"; 0472 static const char SETTING_QUALITY[] = "Image Quality"; 0473 static const char SETTING_INAT_IDS[] = "Write iNat Ids"; 0474 static const char SETTING_TIME_DIFF[] = "Max Time Diff"; 0475 static const char SETTING_DISTANCE[] = "Max Distance"; 0476 static const char SETTING_CLOSEST[] = "Closest Observation"; 0477 static const char SETTING_EXTENDED[] = "Extended Options"; 0478 0479 void INatWindow::readSettings(const QString& uname) 0480 { 0481 KSharedConfigPtr config = KSharedConfig::openConfig(); 0482 QString groupName = QString::fromLatin1("%1 %2 Export Settings"). 0483 arg(d->serviceName, uname); 0484 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Group name is:" << groupName; 0485 KConfigGroup grp = config->group(groupName); 0486 0487 d->resizeCheckBox->setChecked(grp.readEntry(SETTING_RESIZE, true)); 0488 d->dimensionSpinBox->setValue(grp.readEntry(SETTING_DIM, 2048)); 0489 d->imageQualitySpinBox->setValue(grp.readEntry(SETTING_QUALITY, 90)); 0490 d->widget->getPhotoIdCheckBox()->setChecked(grp.readEntry(SETTING_INAT_IDS, false)); 0491 d->photoMaxTimeDiffSpB->setValue(grp.readEntry(SETTING_TIME_DIFF, 5)); 0492 d->photoMaxDistanceSpB->setValue(grp.readEntry(SETTING_DISTANCE, 15)); 0493 d->closestObservationMaxSpB->setValue(grp.readEntry(SETTING_CLOSEST, 500)); 0494 d->moreOptionsButton->setChecked(grp.readEntry(SETTING_EXTENDED, false)); 0495 slotMoreOptionsButton(d->moreOptionsButton->isChecked()); 0496 } 0497 0498 void INatWindow::writeSettings() 0499 { 0500 KSharedConfigPtr config = KSharedConfig::openConfig(); 0501 QString groupName = QString::fromLatin1("%1 %2 Export Settings").arg(d->serviceName, d->username); 0502 0503 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Group name is:" << groupName; 0504 0505 if (QString::compare(QString::fromLatin1("%1 Export Settings").arg(d->serviceName), groupName) == 0) 0506 { 0507 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Not writing entry of group" << groupName; 0508 return; 0509 } 0510 0511 KConfigGroup grp = config->group(groupName); 0512 grp.writeEntry("username", d->username); 0513 grp.writeEntry(SETTING_RESIZE, d->resizeCheckBox->isChecked()); 0514 grp.writeEntry(SETTING_DIM, d->dimensionSpinBox->value()); 0515 grp.writeEntry(SETTING_QUALITY, d->imageQualitySpinBox->value()); 0516 grp.writeEntry(SETTING_INAT_IDS, d->widget->getPhotoIdCheckBox()->isChecked()); 0517 grp.writeEntry(SETTING_TIME_DIFF, d->photoMaxTimeDiffSpB->value()); 0518 grp.writeEntry(SETTING_DISTANCE, d->photoMaxDistanceSpB->value()); 0519 grp.writeEntry(SETTING_CLOSEST, d->closestObservationMaxSpB->value()); 0520 grp.writeEntry(SETTING_EXTENDED, d->moreOptionsButton->isChecked()); 0521 0522 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Entry of group" << groupName << "written"; 0523 } 0524 0525 void INatWindow::slotMoreOptionsButton(bool setting) 0526 { 0527 if (setting) 0528 { 0529 d->moreOptionsButton->setText(i18n("Fewer options")); 0530 d->observationDescription->show(); 0531 d->moreOptionsWidget->show(); 0532 } 0533 else 0534 { 0535 d->moreOptionsButton->setText(i18n("More options")); 0536 d->observationDescription->hide(); 0537 d->moreOptionsWidget->hide(); 0538 } 0539 } 0540 0541 void INatWindow::slotLinkingSucceeded(const QString& username, 0542 const QString& name, const QUrl& iconUrl) 0543 { 0544 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Linking succeeded for user" 0545 << username; 0546 d->username = username; 0547 d->name = name; 0548 d->iconUrl = iconUrl; 0549 d->apiTokenExpiresTimer.start(1000 * std::max(d->talker->apiTokenExpiresIn(), 1)); 0550 0551 if (!d->name.isEmpty() && (d->name != d->username)) 0552 { 0553 d->userNameDisplayLabel->setText(QString::fromLatin1("<b>%1 (%2)</b>"). 0554 arg(d->username, d->name)); 0555 } 0556 else 0557 { 0558 d->userNameDisplayLabel->setText(QString::fromLatin1("<b>%1</b>"). 0559 arg(d->username)); 0560 } 0561 0562 d->widget->updateLabels(username); 0563 KSharedConfigPtr config = KSharedConfig::openConfig(); 0564 0565 Q_FOREACH (const QString& group, config->groupList()) 0566 { 0567 if (!(group.contains(d->serviceName))) 0568 { 0569 continue; 0570 } 0571 0572 KConfigGroup grp = config->group(group); 0573 0574 if (group.contains(d->username)) 0575 { 0576 readSettings(d->username); 0577 break; 0578 } 0579 } 0580 0581 writeSettings(); 0582 0583 if (!d->iconUrl.isEmpty()) 0584 { 0585 d->talker->loadUrl(d->iconUrl); 0586 } 0587 } 0588 0589 void INatWindow::slotLinkingFailed(const QString& error) 0590 { 0591 d->apiTokenExpiresTimer.stop(); 0592 d->accountIcon->hide(); 0593 d->userNameDisplayLabel->setText(i18n("<i>login <b>failed</b></i>")); 0594 d->widget->updateLabels(QString()); 0595 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Linking failed with error" << error; 0596 } 0597 0598 void INatWindow::slotBusy(bool val) 0599 { 0600 setCursor(val ? Qt::WaitCursor : Qt::ArrowCursor); 0601 } 0602 0603 void INatWindow::slotError(const QString& msg) 0604 { 0605 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Error" << msg; 0606 QMessageBox::critical(this, i18nc("@title:window", "Error"), msg); 0607 } 0608 0609 void INatWindow::slotUserChangeRequest() 0610 { 0611 d->apiTokenExpiresTimer.stop(); 0612 writeSettings(); 0613 d->userNameDisplayLabel->setText(QString()); 0614 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Slot Change User Request"; 0615 d->selectUser->reactivate(); 0616 switchUser(); 0617 } 0618 0619 void INatWindow::slotRemoveAccount() 0620 { 0621 d->apiTokenExpiresTimer.stop(); 0622 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Removing user" << d->username; 0623 0624 if (d->username.isEmpty()) 0625 { 0626 return; 0627 } 0628 0629 KSharedConfigPtr config = KSharedConfig::openConfig(); 0630 QString groupName = QString::fromLatin1("%1 %2 Export Settings").arg(d->serviceName, d->username); 0631 KConfigGroup grp = config->group(groupName); 0632 0633 if (grp.exists()) 0634 { 0635 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Removing Account having group" << groupName; 0636 grp.deleteGroup(); 0637 } 0638 0639 d->talker->unLink(); 0640 d->talker->removeUserName(d->serviceName + d->username); 0641 0642 d->accountIcon->hide(); 0643 d->userNameDisplayLabel->setText(QString()); 0644 d->username = QString(); 0645 d->name = QString(); 0646 d->iconUrl = QUrl(); 0647 } 0648 0649 void INatWindow::slotAuthCancel() 0650 { 0651 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Authorization canceled."; 0652 d->apiTokenExpiresTimer.stop(); 0653 d->talker->cancel(); 0654 d->authProgressDlg->hide(); 0655 d->accountIcon->hide(); 0656 d->userNameDisplayLabel->setText(i18n("<i>login <b>canceled</b></i>")); 0657 } 0658 0659 void INatWindow::slotTaxonSelected(const Taxon& taxon, bool fromVision) 0660 { 0661 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Taxon" << taxon.name() << "selected" 0662 << (fromVision ? "from vision." 0663 : "from auto-completion."); 0664 0665 if (d->identification != taxon) 0666 { 0667 d->identification = taxon; 0668 QString name = QLatin1String("<h3>") + taxon.htmlName(); 0669 0670 if (!taxon.commonName().isEmpty()) 0671 { 0672 name += QLatin1String(" (") + taxon.commonName() + 0673 QLatin1String(")"); 0674 } 0675 0676 name += QLatin1String("</h3>"); 0677 d->identificationLabel->setText(name); 0678 d->talker->loadUrl(taxon.squareUrl()); 0679 0680 startButton()->setEnabled(d->observationDateTime.isValid() && 0681 d->latLonValid && !d->inCancel && 0682 (d->imglst->imageUrls().count() <= MAX_OBSERVATION_PHOTOS)); 0683 0684 if (d->latLonValid) 0685 { 0686 d->talker->closestObservation(taxon.id(), 0687 d->latitude, d->longitude); 0688 } 0689 } 0690 0691 d->identificationFromVision = fromVision; 0692 } 0693 0694 void INatWindow::slotComputerVision() 0695 { 0696 const QList<QUrl>& imageUrls = d->imglst->imageUrls(); 0697 0698 if (imageUrls.count()) 0699 { 0700 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Requesting computer-vision id for" 0701 << imageUrls[0].toLocalFile(); 0702 d->talker->computerVision(imageUrls[0]); 0703 } 0704 } 0705 0706 void INatWindow::slotTaxonDeselected() 0707 { 0708 if (d->identification != Taxon()) 0709 { 0710 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Taxon deselected."; 0711 d->identificationFromVision = false; 0712 d->identification = Taxon(); 0713 d->identificationLabel->setText(i18n("<i>no valid identification</i>")); 0714 d->identificationImage->hide(); 0715 slotNearbyObservation(INatTalker::NearbyObservation()); 0716 startButton()->setEnabled(false); 0717 } 0718 } 0719 0720 void INatWindow::slotLoadUrlSucceeded(const QUrl& url, const QByteArray& data) 0721 { 0722 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Image" << url << "received."; 0723 0724 if (url == d->identification.squareUrl()) 0725 { 0726 QImage image; 0727 image.loadFromData(data); 0728 d->identificationImage->setPixmap(QPixmap::fromImage(image)); 0729 d->identificationImage->show(); 0730 } 0731 else if (url == d->iconUrl) 0732 { 0733 QImage image; 0734 image.loadFromData(data); 0735 d->accountIcon->setPixmap(QPixmap::fromImage(image)); 0736 d->accountIcon->show(); 0737 } 0738 } 0739 0740 void INatWindow::updateProgressBarValue(unsigned diff) 0741 { 0742 int value = d->widget->progressBar()->value(); 0743 value += diff; 0744 d->widget->progressBar()->setValue(value); 0745 0746 if (value == d->widget->progressBar()->maximum()) 0747 { 0748 d->widget->progressBar()->reset(); 0749 setUiInProgressState(false); 0750 } 0751 } 0752 0753 void INatWindow::updateProgressBarMaximum(unsigned diff) 0754 { 0755 if (d->widget->progressBar()->isHidden()) 0756 { 0757 d->widget->progressBar()->setMaximum(diff); 0758 d->widget->progressBar()->setValue(0); 0759 setUiInProgressState(true); 0760 d->widget->progressBar()->progressScheduled(i18n("iNaturalist Export"), true, true); 0761 d->widget->progressBar()->progressThumbnailChanged(QIcon::fromTheme(QLatin1String("dk-inat")).pixmap(22, 22)); 0762 } 0763 else 0764 { 0765 int maximum = d->widget->progressBar()->maximum(); 0766 d->widget->progressBar()->setMaximum(maximum + diff); 0767 } 0768 } 0769 0770 /** 0771 * This slot is called when 'Start Uploading' button is pressed. 0772 */ 0773 void INatWindow::slotUser1() 0774 { 0775 if (d->imglst->imageUrls().isEmpty() || 0776 !d->latLonValid || 0777 d->inCancel || 0778 (d->imglst->imageUrls().count() > MAX_OBSERVATION_PHOTOS) || 0779 !d->observationDateTime.isValid() || 0780 !d->identification.isValid()) 0781 { 0782 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "NOT uploading observation."; 0783 return; 0784 } 0785 0786 startButton()->setEnabled(false); 0787 0788 // Create an observation. 0789 0790 QString obsDateTime = d->observationDateTime.toString(Qt::ISODate); 0791 QJsonObject params; 0792 params.insert(QLatin1String("observed_on_string"), QJsonValue(obsDateTime)); 0793 params.insert(QLatin1String("time_zone"), 0794 QJsonValue(QLatin1String(QTimeZone::systemTimeZoneId()))); 0795 params.insert(QLatin1String("latitude"), QJsonValue(d->latitude)); 0796 params.insert(QLatin1String("longitude"),QJsonValue(d->longitude)); 0797 params.insert(QLatin1String("taxon_id"), 0798 QJsonValue(d->identification.id())); 0799 0800 QString description = d->observationDescription->toPlainText().trimmed(); 0801 0802 if (!description.isEmpty()) 0803 { 0804 params.insert(QLatin1String("description"), QJsonValue(description)); 0805 } 0806 0807 QString placeName = d->placesComboBox->currentText().simplified(); 0808 if (placeName != d->placesComboBox->currentText()) 0809 { 0810 d->placesComboBox->setEditText(placeName); 0811 } 0812 0813 if (!placeName.isEmpty()) 0814 { 0815 params.insert(QLatin1String("place_guess"), QJsonValue(placeName)); 0816 saveEditedPlaceName(placeName); 0817 } 0818 0819 params.insert(QLatin1String("owners_identification_from_vision"), 0820 QJsonValue(d->identificationFromVision)); 0821 0822 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Creating observation of" 0823 << d->identification.name() << "from" 0824 << obsDateTime << "with" 0825 << d->imglst->imageUrls().count() 0826 << ((d->imglst->imageUrls().count() == 1) ? "picture." 0827 : "pictures."); 0828 QJsonObject jsonObservation; 0829 jsonObservation.insert(QLatin1String("observation"), QJsonValue(params)); 0830 updateProgressBarMaximum(1 + d->imglst->imageUrls().count()); 0831 0832 INatTalker::PhotoUploadRequest request(d->imglst->imageUrls(), 0833 d->widget->getPhotoIdCheckBox()-> 0834 isChecked(), 0835 d->resizeCheckBox->isChecked(), 0836 d->dimensionSpinBox->value(), 0837 d->imageQualitySpinBox->value(), 0838 d->username); 0839 d->talker->createObservation(QJsonDocument(jsonObservation).toJson(), 0840 request); 0841 0842 // Clear data, user can start working on the next observation right away. 0843 0844 d->imglst->listView()->clear(); 0845 slotTaxonDeselected(); 0846 d->identificationEdit->clear(); 0847 d->observationDescription->clear(); 0848 } 0849 0850 void INatWindow::cancelUpload(const INatTalker::PhotoUploadRequest& request) 0851 { 0852 updateProgressBarMaximum(1); 0853 updateProgressBarValue(request.m_images.count()); 0854 d->talker->deleteObservation(request.m_observationId, request.m_apiKey); 0855 0856 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Upload canceled, deleting observation" 0857 << request.m_observationId; 0858 } 0859 0860 void INatWindow::slotObservationCreated(const INatTalker::PhotoUploadRequest& 0861 request) 0862 { 0863 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Observation" 0864 << request.m_observationId 0865 << "created, uploading first picture."; 0866 updateProgressBarValue(1); 0867 0868 if (d->inCancel) 0869 { 0870 cancelUpload(request); 0871 } 0872 else if (!request.m_images.isEmpty()) 0873 { 0874 d->talker->uploadNextPhoto(request); 0875 } 0876 } 0877 0878 void INatWindow::slotPhotoUploaded(const INatTalker::PhotoUploadResult& result) 0879 { 0880 updateProgressBarValue(1); 0881 0882 INatTalker::PhotoUploadRequest request(result.m_request); 0883 0884 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Photo" 0885 << request.m_images.front().toLocalFile() 0886 << "uploaded, observation" 0887 << request.m_observationId 0888 << "observation photo" 0889 << result.m_observationPhotoId 0890 << "photo" << result.m_photoId; 0891 0892 if (d->inCancel) 0893 { 0894 request.m_images.pop_front(); 0895 cancelUpload(request); 0896 return; 0897 } 0898 0899 if (request.m_updateIds) 0900 { 0901 const QUrl& fileUrl = request.m_images.front(); 0902 DMetadata meta; 0903 0904 if (meta.supportXmp() && 0905 meta.canWriteXmp(fileUrl.toLocalFile()) && 0906 meta.load(fileUrl.toLocalFile())) 0907 { 0908 if (!d->xmpNameSpace) 0909 { 0910 meta.registerXmpNameSpace(xmpNameSpaceURI, xmpNameSpacePrefix); 0911 d->xmpNameSpace = true; 0912 } 0913 0914 meta.setXmpTagString("Xmp.iNaturalist.observation", 0915 QString::number(request.m_observationId)); 0916 meta.setXmpTagString("Xmp.iNaturalist.observationPhoto", 0917 QString::number(result.m_observationPhotoId)); 0918 meta.setXmpTagString("Xmp.iNaturalist.photo", 0919 QString::number(result.m_photoId)); 0920 meta.save(fileUrl.toLocalFile()); 0921 } 0922 } 0923 0924 request.m_images.pop_front(); 0925 0926 if (!request.m_images.isEmpty()) 0927 { 0928 qCDebug(DIGIKAM_WEBSERVICES_LOG) 0929 << "Uploading next photo" << request.m_images.front().toLocalFile() 0930 << "to observation" << request.m_observationId; 0931 d->talker->uploadNextPhoto(request); 0932 } 0933 } 0934 0935 void INatWindow::slotObservationDeleted(int observationId) 0936 { 0937 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Observation" << observationId 0938 << "deleted."; 0939 updateProgressBarValue(1); 0940 0941 if (!d->talker->stillUploading()) 0942 { 0943 d->inCancel = false; 0944 slotBusy(false); 0945 } 0946 } 0947 0948 void INatWindow::slotNearbyPlaces(const QStringList& places) 0949 { 0950 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Received" << places.count() << 0951 "nearby places," << d->editedPlaces.count() << "edited places."; 0952 0953 QString selected = d->placesComboBox->currentText(); 0954 d->placesComboBox->clear(); 0955 0956 for (auto& place : d->editedPlaces) 0957 { 0958 d->placesComboBox->addItem(place); 0959 0960 if (place == selected) 0961 { 0962 // Keep previous selection if it is still an option. 0963 0964 d->placesComboBox->setCurrentText(selected); 0965 } 0966 } 0967 0968 for (auto& place : places) 0969 { 0970 d->placesComboBox->addItem(place); 0971 0972 if (place == selected) 0973 { 0974 // Keep previous selection if it is still an option. 0975 0976 d->placesComboBox->setCurrentText(selected); 0977 } 0978 } 0979 } 0980 0981 void INatWindow::slotNearbyObservation(const INatTalker::NearbyObservation& 0982 nearbyObservation) 0983 { 0984 if (!nearbyObservation.isValid()) 0985 { 0986 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "No valid nearby observation."; 0987 0988 d->closestKnownObservation->clear(); 0989 d->closestKnownObservation->hide(); 0990 0991 return; 0992 } 0993 0994 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Received nearby observation."; 0995 0996 QString red1; 0997 QString red2; 0998 0999 if (nearbyObservation.m_distanceMeters > d->closestObservationMaxSpB->value()) 1000 { 1001 red1 = QLatin1String("<font color=\"red\">"); 1002 red2 = QLatin1String("</font>"); 1003 } 1004 1005 QString distance(red1 + localizedDistance(nearbyObservation. 1006 m_distanceMeters, 'f', 1) + red2); 1007 1008 QString observation(QString(QLatin1String("<a href=\"https://www.inatura" 1009 "list.org/observations/%1\">")). 1010 arg(nearbyObservation.m_observationId) + 1011 i18n("observation") + QLatin1String("</a>")); 1012 1013 QString obscured; 1014 1015 if (nearbyObservation.m_obscured) 1016 { 1017 obscured = QLatin1String("<em>") + i18nc("location", "obscured") + 1018 QLatin1String("</em> "); 1019 } 1020 1021 QString text(i18n("Closest %1research-grade %2 reported in %3.", 1022 obscured, observation, distance)); 1023 d->closestKnownObservation->setText(text); 1024 d->closestKnownObservation->show(); 1025 } 1026 1027 void INatWindow::slotImageListChanged() 1028 { 1029 static const QLatin1Char lf('\n'); 1030 1031 // number of digits printed for latitude and longitude 1032 1033 enum 1034 { 1035 COORD_PRECISION = 5 1036 }; 1037 1038 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Updating image list."; 1039 1040 bool latLonValid = false; 1041 double latitude = 0.0; 1042 double longitude = 0.0; 1043 QDateTime observationTime; 1044 1045 DItemsListView* const listView = d->widget->d->imglst->listView(); 1046 1047 for (auto& url : d->imglst->imageUrls()) 1048 { 1049 if (url.isEmpty()) 1050 { 1051 continue; 1052 } 1053 1054 DItemInfo info(d->iface->itemInfo(url)); 1055 DItemsListViewItem* const item = listView->findItem(url); 1056 QDateTime dateTime = info.dateTime(); 1057 QString dt; 1058 1059 if (dateTime.isValid()) 1060 { 1061 // show date and time of photo, our observation 1062 1063 if (!observationTime.isValid()) 1064 { 1065 observationTime = dateTime; 1066 dt = QLocale().toString(dateTime, QLocale::ShortFormat) + lf + 1067 i18n("observation time"); 1068 QBrush brush(Qt::black); 1069 item->setForeground(ItemDate, brush); 1070 } 1071 else 1072 { 1073 // show time difference from observation 1074 1075 int difference = int(qAbs(dateTime.secsTo(observationTime))); 1076 dt = localizedTimeDifference(difference) + lf + 1077 i18n("from observation"); 1078 QBrush brush((difference > 60 * d->photoMaxTimeDiffSpB->value()) ? Qt::red : Qt::black); 1079 item->setForeground(ItemDate, brush); 1080 } 1081 } 1082 else 1083 { 1084 dt = i18n("not available"); 1085 QFont font = item->font(ItemDate); 1086 font.setItalic(true); 1087 item->setFont(ItemDate, font); 1088 } 1089 1090 item->setText(ItemDate, dt); 1091 1092 QString gps; 1093 1094 if (info.hasGeolocationInfo()) 1095 { 1096 if (latLonValid) 1097 { 1098 // show distance from observation coordinates 1099 1100 double distance = distanceBetween(latitude, longitude, 1101 info.latitude(), 1102 info.longitude()); 1103 gps = localizedDistance(distance,'f', 0) + lf + 1104 i18n("from observation"); 1105 QBrush brush((distance > d->photoMaxDistanceSpB->value()) ? Qt::red : Qt::black); 1106 item->setForeground(ItemLocation, brush); 1107 } 1108 else 1109 { 1110 // show gps coordinates of photo, our observation coordinates 1111 1112 latLonValid = true; 1113 latitude = info.latitude(); 1114 longitude = info.longitude(); 1115 gps = localizedLocation(latitude, longitude, COORD_PRECISION) + 1116 lf + i18n("observation location"); 1117 QBrush brush(Qt::black); 1118 item->setForeground(ItemLocation, brush); 1119 } 1120 } 1121 else 1122 { 1123 // gps coordinates not available 1124 1125 gps = i18n("not available"); 1126 QFont font = item->font(ItemLocation); 1127 font.setItalic(true); 1128 item->setFont(ItemLocation, font); 1129 } 1130 1131 item->setText(ItemLocation, gps); 1132 } 1133 1134 if ((d->latLonValid != latLonValid) || 1135 (d->latitude != latitude) || 1136 (d->longitude != longitude)) 1137 { 1138 if (latLonValid) 1139 { 1140 d->talker->nearbyPlaces(latitude, longitude); 1141 } 1142 else 1143 { 1144 slotNearbyPlaces(QStringList()); 1145 } 1146 } 1147 1148 d->latLonValid = latLonValid; 1149 d->latitude = latitude; 1150 d->longitude = longitude; 1151 d->observationDateTime = observationTime; 1152 1153 startButton()->setEnabled(observationTime.isValid() && latLonValid && 1154 d->identification.isValid() && !d->inCancel && 1155 (d->imglst->imageUrls().count() <= MAX_OBSERVATION_PHOTOS)); 1156 } 1157 1158 void INatWindow::slotValueChanged(int) 1159 { 1160 // Called upon change to d->photoMaxDistanceSpB or d->photoMaxTimeDiffSpB. 1161 1162 slotImageListChanged(); 1163 } 1164 1165 void INatWindow::slotClosestChanged(int) 1166 { 1167 // Called upon change to d->closestObservationMaxSpB 1168 1169 if (d->latLonValid && d->identification.isValid()) 1170 { 1171 d->talker->closestObservation(d->identification.id(), 1172 d->latitude, d->longitude); 1173 } 1174 else 1175 { 1176 d->closestKnownObservation->clear(); 1177 } 1178 } 1179 1180 void INatWindow::saveEditedPlaceName(const QString& text) 1181 { 1182 if (!d->editedPlaces.contains(text)) 1183 { 1184 for (int i = 0; i < d->placesComboBox->count(); ++i) 1185 { 1186 if (d->placesComboBox->itemText(i) == text) 1187 { 1188 return; 1189 } 1190 } 1191 } 1192 1193 d->editedPlaces.removeOne(text); 1194 d->editedPlaces.push_front(text); 1195 1196 if (d->editedPlaces.count() > MAX_EDITED_PLACES) 1197 { 1198 d->editedPlaces.removeLast(); 1199 } 1200 } 1201 1202 } // namespace DigikamGenericINatPlugin 1203 1204 #include "moc_inatwindow.cpp"