File indexing completed on 2025-01-19 03:58:12
0001 /* ============================================================ 0002 * 0003 * This file is a part of digiKam project 0004 * https://www.digikam.org 0005 * 0006 * Date : 2010-07-20 0007 * Description : GPS search marker tiler 0008 * 0009 * SPDX-FileCopyrightText: 2010 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de> 0010 * SPDX-FileCopyrightText: 2010 by Gabriel Voicu <ping dot gabi at gmail dot com> 0011 * SPDX-FileCopyrightText: 2010-2011 by Michael G. Hansen <mike at mghansen dot de> 0012 * SPDX-FileCopyrightText: 2015 by Mohamed_Anwer <m_dot_anwer at gmx dot com> 0013 * 0014 * SPDX-License-Identifier: GPL-2.0-or-later 0015 * 0016 * ============================================================ */ 0017 0018 #include "gpsmarkertiler.h" 0019 0020 // Qt includes 0021 0022 #include <QPair> 0023 #include <QRectF> 0024 #include <QTimer> 0025 0026 // Local includes 0027 0028 #include "groupstatecomputer.h" 0029 #include "gpsiteminfosorter.h" 0030 #include "dnotificationwrapper.h" 0031 #include "digikamapp.h" 0032 #include "digikam_debug.h" 0033 #include "dbjobsmanager.h" 0034 0035 /// @todo Actually use this definition! 0036 typedef QPair<Digikam::TileIndex, int> MapPair; 0037 0038 Q_DECLARE_METATYPE(MapPair) 0039 0040 namespace Digikam 0041 { 0042 0043 /** 0044 * @class GPSMarkerTiler 0045 * 0046 * @brief Marker model for storing data needed to display markers on the map. The data is retrieved from Digikam's database. 0047 */ 0048 0049 class Q_DECL_HIDDEN GPSMarkerTiler::MyTile : public Tile 0050 { 0051 public: 0052 0053 MyTile() = default; 0054 0055 QList<qlonglong> imagesId; 0056 0057 private: 0058 0059 ~MyTile() = delete; 0060 }; 0061 0062 class Q_DECL_HIDDEN GPSMarkerTiler::Private 0063 { 0064 public: 0065 0066 class Q_DECL_HIDDEN InternalJobs 0067 { 0068 public: 0069 0070 InternalJobs() 0071 : level (0), 0072 jobThread (nullptr), 0073 dataFromDatabase() 0074 { 0075 } 0076 0077 int level; 0078 GPSDBJobsThread* jobThread; 0079 QList<GPSItemInfo> dataFromDatabase; 0080 }; 0081 0082 explicit Private() 0083 : jobs (), 0084 thumbnailLoadThread (nullptr), 0085 thumbnailMap (), 0086 rectList (), 0087 activeState (true), 0088 imagesHash (), 0089 imageFilterModel (), 0090 imageAlbumModel (), 0091 selectionModel (), 0092 currentRegionSelection(), 0093 mapGlobalGroupState () 0094 { 0095 } 0096 0097 QList<InternalJobs> jobs; 0098 ThumbnailLoadThread* thumbnailLoadThread; 0099 QHash<qlonglong, QVariant> thumbnailMap; 0100 QList<QRectF> rectList; 0101 bool activeState; 0102 QHash<qlonglong, GPSItemInfo> imagesHash; 0103 ItemFilterModel* imageFilterModel; 0104 ItemAlbumModel* imageAlbumModel; 0105 QItemSelectionModel* selectionModel; 0106 GeoCoordinates::Pair currentRegionSelection; 0107 GeoGroupState mapGlobalGroupState; 0108 }; 0109 0110 /** 0111 * @brief Constructor 0112 * @param parent the parent object 0113 */ 0114 GPSMarkerTiler::GPSMarkerTiler(QObject* const parent, 0115 ItemFilterModel* const imageFilterModel, 0116 QItemSelectionModel* const selectionModel) 0117 : AbstractMarkerTiler(parent), 0118 d (new Private()) 0119 { 0120 resetRootTile(); 0121 0122 d->thumbnailLoadThread = new ThumbnailLoadThread(this); 0123 d->imageFilterModel = imageFilterModel; 0124 d->imageAlbumModel = qobject_cast<ItemAlbumModel*>(imageFilterModel->sourceModel()); 0125 d->selectionModel = selectionModel; 0126 0127 connect(d->thumbnailLoadThread, SIGNAL(signalThumbnailLoaded(LoadingDescription,QPixmap)), 0128 this, SLOT(slotThumbnailLoaded(LoadingDescription,QPixmap))); 0129 0130 connect(CoreDbAccess::databaseWatch(), SIGNAL(imageChange(ImageChangeset)), 0131 this, SLOT(slotImageChange(ImageChangeset)), Qt::QueuedConnection); 0132 0133 connect(d->imageAlbumModel, SIGNAL(imageInfosAdded(QList<ItemInfo>)), 0134 this, SLOT(slotNewModelData(QList<ItemInfo>))); 0135 0136 connect(d->selectionModel, SIGNAL(selectionChanged(QItemSelection,QItemSelection)), 0137 this, SLOT(slotSelectionChanged(QItemSelection,QItemSelection))); 0138 } 0139 0140 /** 0141 * @brief Destructor 0142 */ 0143 GPSMarkerTiler::~GPSMarkerTiler() 0144 { 0145 delete d; 0146 } 0147 0148 void GPSMarkerTiler::regenerateTiles() 0149 { 0150 } 0151 0152 /** 0153 * @brief Requests all images inside a given rectangle from the database. 0154 * 0155 * This function calls the database for the images found inside a rectangle 0156 * defined by upperLeft and lowerRight points. The images are returned from 0157 * the database in batches. 0158 * 0159 * @param upperLeft The North-West point. 0160 * @param lowerRight The South-East point. 0161 * @param level The requested tiling level. 0162 */ 0163 void GPSMarkerTiler::prepareTiles(const GeoCoordinates& upperLeft, const GeoCoordinates& lowerRight, int level) 0164 { 0165 const QRectF worldRect(-90,-180,180,360); 0166 0167 qreal lat1 = upperLeft.lat(); 0168 qreal lng1 = upperLeft.lon(); 0169 qreal lat2 = lowerRight.lat(); 0170 qreal lng2 = lowerRight.lon(); 0171 auto requestedRect = worldRect.intersected(QRectF(lat1, lng1, lat2 - lat1, lng2 - lng1)); 0172 0173 for (int i = 0 ; i < d->rectList.count() ; ++i) 0174 { 0175 // is there a rect that contains the requested one? 0176 const QRectF& currentRect = d->rectList.at(i); 0177 0178 if (currentRect.contains(requestedRect)) 0179 { 0180 return; 0181 } 0182 0183 // and remove rects that are contained in the requested one 0184 0185 if (requestedRect.contains(currentRect)) 0186 { 0187 std::swap(d->rectList[i], d->rectList.back()); 0188 d->rectList.removeLast(); 0189 // we removed one entry. we have to subtract one from the index 0190 --i; 0191 } 0192 } 0193 0194 // grow the rect a bit such that we don't have to request many small ones while panning 0195 qreal marginW = requestedRect.width() * 0.05; 0196 qreal marginH = requestedRect.height() * 0.05; 0197 requestedRect = requestedRect.marginsAdded(QMarginsF(marginW, marginH, marginW, marginH)); 0198 requestedRect = worldRect.intersected(requestedRect); 0199 requestedRect.getCoords(&lat1, &lng1, &lat2, &lng2); 0200 0201 for (int i = 0 ; i < d->rectList.count() ; ++i) 0202 { 0203 qreal rectLat1, rectLng1, rectLat2, rectLng2; 0204 const QRectF currentRect = d->rectList.at(i); 0205 currentRect.getCoords(&rectLat1, &rectLng1, &rectLat2, &rectLng2); 0206 0207 if (currentRect.contains(lat1, lng1)) 0208 { 0209 if (currentRect.contains(lat2, lng1)) 0210 { 0211 lng1 = rectLng2; 0212 break; 0213 } 0214 } 0215 else if (currentRect.contains(lat2, lng1)) 0216 { 0217 if (currentRect.contains(lat2, lng2)) 0218 { 0219 lat2 = rectLat1; 0220 break; 0221 } 0222 } 0223 else if (currentRect.contains(lat2, lng2)) 0224 { 0225 if (currentRect.contains(lat1, lng2)) 0226 { 0227 lng2 = rectLng1; 0228 break; 0229 } 0230 } 0231 else if (currentRect.contains(lat1, lng2)) 0232 { 0233 if (currentRect.contains(lat1, lng1)) 0234 { 0235 lat1 = rectLat2; 0236 break; 0237 } 0238 } 0239 } 0240 0241 requestedRect = QRectF(lat1, lng1, lat2 - lat1, lng2 - lng1); 0242 d->rectList.append(requestedRect); 0243 0244 qCDebug(DIGIKAM_GENERAL_LOG) << "Listing" << lat1 << lat2 << lng1 << lng2; 0245 0246 GPSDBJobInfo jobInfo; 0247 jobInfo.setLat1(lat1); 0248 jobInfo.setLat2(lat2); 0249 jobInfo.setLng1(lng1); 0250 jobInfo.setLng2(lng2); 0251 0252 GPSDBJobsThread* const currentJob = DBJobsManager::instance()->startGPSJobThread(jobInfo); 0253 0254 Private::InternalJobs currentJobInfo; 0255 0256 currentJobInfo.jobThread = currentJob; 0257 currentJobInfo.level = level; 0258 0259 d->jobs.append(currentJobInfo); 0260 0261 connect(currentJob, SIGNAL(finished()), 0262 this, SLOT(slotMapImagesJobResult())); 0263 0264 connect(currentJob, SIGNAL(data(QList<ItemListerRecord>)), 0265 this, SLOT(slotMapImagesJobData(QList<ItemListerRecord>))); 0266 } 0267 0268 /** 0269 * @brief Returns a pointer to a tile. 0270 * @param tileIndex The index of a tile. 0271 * @param stopIfEmpty Determines whether child tiles are also created for empty tiles. 0272 */ 0273 AbstractMarkerTiler::Tile* GPSMarkerTiler::getTile(const TileIndex& tileIndex, const bool stopIfEmpty) 0274 { 0275 Q_ASSERT(tileIndex.level() <= TileIndex::MaxLevel); 0276 0277 MyTile* tile = static_cast<MyTile*>(rootTile()); 0278 0279 for (int level = 0 ; level < tileIndex.indexCount() ; ++level) 0280 { 0281 const int currentIndex = tileIndex.linearIndex(level); 0282 MyTile* childTile = nullptr; 0283 0284 if (tile->childrenEmpty()) 0285 { 0286 for (int i = 0 ; i < tile->imagesId.count() ; ++i) 0287 { 0288 const int currentImageId = tile->imagesId.at(i); 0289 const GPSItemInfo currentItemInfo = d->imagesHash[currentImageId]; 0290 const TileIndex markerTileIndex = TileIndex::fromCoordinates(currentItemInfo.coordinates, level); 0291 const int newTileIndex = markerTileIndex.lastIndex(); 0292 MyTile* const newTile1 = static_cast<MyTile*>(tile->getChild(newTileIndex)); 0293 0294 if (newTile1 == nullptr) 0295 { 0296 MyTile* const newTile2 = static_cast<MyTile*>(tileNew()); 0297 newTile2->imagesId.append(currentImageId); 0298 tile->addChild(newTileIndex, newTile2); 0299 } 0300 else 0301 { 0302 if (!newTile1->imagesId.contains(currentImageId)) 0303 { 0304 newTile1->imagesId.append(currentImageId); 0305 } 0306 } 0307 } 0308 } 0309 0310 childTile = static_cast<MyTile*>(tile->getChild(currentIndex)); 0311 0312 if (childTile == nullptr) 0313 { 0314 if (stopIfEmpty) 0315 { 0316 // there will be no markers in this tile, therefore stop 0317 0318 return nullptr; 0319 } 0320 0321 childTile = static_cast<MyTile*>(tileNew()); 0322 tile->addChild(currentIndex, childTile); 0323 } 0324 0325 tile = childTile; 0326 } 0327 0328 return tile; 0329 } 0330 0331 int GPSMarkerTiler::getTileMarkerCount(const TileIndex& tileIndex) 0332 { 0333 MyTile* const tile = static_cast<MyTile*>(getTile(tileIndex, true)); 0334 0335 if (tile) 0336 { 0337 return tile->imagesId.count(); 0338 } 0339 0340 return 0; 0341 } 0342 0343 int GPSMarkerTiler::getTileSelectedCount(const TileIndex& tileIndex) 0344 { 0345 Q_UNUSED(tileIndex) 0346 0347 return 0; 0348 } 0349 0350 /** 0351 * @brief This function finds the best representative marker from a tile of markers. 0352 * @param tileIndex Index of the tile from which the best marker should be found. 0353 * @param sortKey Sets the criteria for selecting the representative thumbnail, a combination of the SortOptions bits. 0354 * @return Returns the internally used index of the marker. 0355 */ 0356 QVariant GPSMarkerTiler::getTileRepresentativeMarker(const TileIndex& tileIndex, const int sortKey) 0357 { 0358 MyTile* const tile = static_cast<MyTile*>(getTile(tileIndex, true)); 0359 0360 if (!tile) 0361 { 0362 return QVariant(); 0363 } 0364 0365 if (tile->imagesId.isEmpty()) 0366 { 0367 return QVariant(); 0368 } 0369 0370 GPSItemInfo bestMarkerInfo = d->imagesHash.value(tile->imagesId.first()); 0371 GeoGroupState bestMarkerGroupState = getImageState(bestMarkerInfo.id); 0372 0373 for (int i = 1 ; i < tile->imagesId.count() ; ++i) 0374 { 0375 const GPSItemInfo currentMarkerInfo = d->imagesHash.value(tile->imagesId.at(i)); 0376 const GeoGroupState currentMarkerGroupState = getImageState(currentMarkerInfo.id); 0377 0378 if (GPSItemInfoSorter::fitsBetter(bestMarkerInfo, 0379 bestMarkerGroupState, 0380 currentMarkerInfo, 0381 currentMarkerGroupState, 0382 getGlobalGroupState(), 0383 GPSItemInfoSorter::SortOptions(sortKey))) 0384 { 0385 bestMarkerInfo = currentMarkerInfo; 0386 bestMarkerGroupState = currentMarkerGroupState; 0387 } 0388 } 0389 0390 const QPair<TileIndex, int> returnedMarker(tileIndex, bestMarkerInfo.id); 0391 0392 return QVariant::fromValue(returnedMarker); 0393 } 0394 0395 /** 0396 * @brief This function finds the best representative marker from a group of markers. This is needed to display a thumbnail for a marker group. 0397 * @param indices A list containing markers, obtained by getTileRepresentativeMarker. 0398 * @param sortKey Sets the criteria for selecting the representative thumbnail, a combination of the SortOptions bits. 0399 * @return Returns the internally used index of the marker. 0400 */ 0401 QVariant GPSMarkerTiler::bestRepresentativeIndexFromList(const QList<QVariant>& indices, const int sortKey) 0402 { 0403 if (indices.isEmpty()) 0404 { 0405 return QVariant(); 0406 } 0407 0408 const QPair<TileIndex, int> firstIndex = indices.first().value<QPair<TileIndex, int> >(); 0409 GPSItemInfo bestMarkerInfo = d->imagesHash.value(firstIndex.second); 0410 GeoGroupState bestMarkerGroupState = getImageState(firstIndex.second); 0411 TileIndex bestMarkerTileIndex = firstIndex.first; 0412 0413 for (int i = 1 ; i < indices.count() ; ++i) 0414 { 0415 const QPair<TileIndex, int> currentIndex = indices.at(i).value<QPair<TileIndex, int> >(); 0416 0417 GPSItemInfo currentMarkerInfo = d->imagesHash.value(currentIndex.second); 0418 GeoGroupState currentMarkerGroupState = getImageState(currentIndex.second); 0419 0420 if (GPSItemInfoSorter::fitsBetter(bestMarkerInfo, 0421 bestMarkerGroupState, 0422 currentMarkerInfo, 0423 currentMarkerGroupState, 0424 getGlobalGroupState(), 0425 GPSItemInfoSorter::SortOptions(sortKey))) 0426 { 0427 bestMarkerInfo = currentMarkerInfo; 0428 bestMarkerGroupState = currentMarkerGroupState; 0429 bestMarkerTileIndex = currentIndex.first; 0430 } 0431 } 0432 0433 const QPair<TileIndex, int> returnedMarker(bestMarkerTileIndex, bestMarkerInfo.id); 0434 0435 return QVariant::fromValue(returnedMarker); 0436 } 0437 0438 /** 0439 * @brief This function retrieves the thumbnail for an index. 0440 * @param index The marker's index. 0441 * @param size The size of the thumbnail. 0442 * @return If the thumbnail has been loaded in the ThumbnailLoadThread instance, it is returned. 0443 * If not, a QPixmap is returned and ThumbnailLoadThread's signal named signalThumbnailLoaded is emitted when the thumbnail becomes available. 0444 */ 0445 QPixmap GPSMarkerTiler::pixmapFromRepresentativeIndex(const QVariant& index, const QSize& size) 0446 { 0447 QPair<TileIndex, int> indexForPixmap = index.value<QPair<TileIndex, int> >(); 0448 0449 QPixmap thumbnail; 0450 ItemInfo info(indexForPixmap.second); 0451 d->thumbnailMap.insert(info.id(), index); 0452 0453 if (d->thumbnailLoadThread->find(info.thumbnailIdentifier(), thumbnail, qMax(size.width() + 2, size.height() + 2))) 0454 { 0455 // digikam returns thumbnails with a border around them, 0456 // but geolocation interface expects them without a border 0457 0458 return thumbnail.copy(1, 1, thumbnail.size().width() - 2, thumbnail.size().height() - 2); 0459 } 0460 else 0461 { 0462 return QPixmap(); 0463 } 0464 } 0465 0466 /** 0467 * @brief This function compares two marker indices. 0468 */ 0469 bool GPSMarkerTiler::indicesEqual(const QVariant& a, const QVariant& b) const 0470 { 0471 QPair<TileIndex, int> firstIndex = a.value<QPair<TileIndex, int> >(); 0472 QPair<TileIndex, int> secondIndex = b.value<QPair<TileIndex, int> >(); 0473 0474 QList<int> aIndicesList = firstIndex.first.toIntList(); 0475 QList<int> bIndicesList = secondIndex.first.toIntList(); 0476 0477 if ((firstIndex.second == secondIndex.second) && (aIndicesList == bIndicesList)) 0478 { 0479 return true; 0480 } 0481 0482 return false; 0483 } 0484 0485 GeoGroupState GPSMarkerTiler::getTileGroupState(const TileIndex& tileIndex) 0486 { 0487 const bool haveGlobalSelection = (d->mapGlobalGroupState & (FilteredPositiveMask | RegionSelectedMask)); 0488 0489 if (!haveGlobalSelection) 0490 { 0491 return SelectedNone; 0492 } 0493 0494 /// @todo Store this state in the tiles! 0495 0496 MyTile* const tile = static_cast<MyTile*>(getTile(tileIndex, true)); 0497 GroupStateComputer tileStateComputer; 0498 0499 for (int i = 0 ; i < tile->imagesId.count() ; ++i) 0500 { 0501 const GeoGroupState imageState = getImageState(tile->imagesId.at(i)); 0502 0503 tileStateComputer.addState(imageState); 0504 } 0505 0506 return tileStateComputer.getState(); 0507 } 0508 0509 /** 0510 * @brief The marker data is returned from the database in batches. This function takes and unites the batches. 0511 */ 0512 void GPSMarkerTiler::slotMapImagesJobData(const QList<ItemListerRecord>& records) 0513 { 0514 if (records.isEmpty()) 0515 { 0516 return; 0517 } 0518 0519 Private::InternalJobs* internalJob = nullptr; 0520 0521 for (int i = 0 ; i < d->jobs.count() ; ++i) 0522 { 0523 if (sender() == d->jobs.at(i).jobThread) 0524 { 0525 /// @todo Is this really safe? 0526 0527 internalJob = &d->jobs[i]; 0528 break; 0529 } 0530 } 0531 0532 if (!internalJob) 0533 { 0534 return; 0535 } 0536 0537 Q_FOREACH (const ItemListerRecord &record, records) 0538 { 0539 if (record.extraValues.count() < 2) 0540 { 0541 // skip info without coordinates 0542 0543 continue; 0544 } 0545 0546 GPSItemInfo entry; 0547 0548 entry.id = record.imageID; 0549 entry.rating = record.rating; 0550 entry.dateTime = record.creationDate; 0551 entry.coordinates.setLatLon(record.extraValues.first().toDouble(), record.extraValues.last().toDouble()); 0552 0553 internalJob->dataFromDatabase << entry; 0554 } 0555 } 0556 0557 /** 0558 * @brief Now, all the marker data has been retrieved from the database. Here, the markers are sorted into tiles. 0559 */ 0560 void GPSMarkerTiler::slotMapImagesJobResult() 0561 { 0562 int foundIndex = -1; 0563 0564 for (int i = 0 ; i < d->jobs.count() ; ++i) 0565 { 0566 if (sender() == d->jobs.at(i).jobThread) 0567 { 0568 foundIndex = i; 0569 break; 0570 } 0571 } 0572 0573 if (foundIndex < 0) 0574 { 0575 // this should not happen, but ok... 0576 0577 return; 0578 } 0579 0580 if (d->jobs.at(foundIndex).jobThread->hasErrors()) 0581 { 0582 const QString& err = d->jobs.at(foundIndex).jobThread->errorsList().first(); 0583 0584 qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to list images in selected area: " 0585 << err; 0586 0587 // Pop-up a message about the error. 0588 0589 DNotificationWrapper(QString(), err, 0590 DigikamApp::instance(), DigikamApp::instance()->windowTitle()); 0591 } 0592 0593 // get the results from the job: 0594 0595 const QList<GPSItemInfo> returnedItemInfo = d->jobs.at(foundIndex).dataFromDatabase; 0596 0597 /// @todo Currently, we ignore the wanted level and just add the images 0598 /* 0599 const int wantedLevel = d->jobs.at(foundIndex).level; 0600 */ 0601 // remove the finished job 0602 0603 d->jobs[foundIndex].jobThread->cancel(); 0604 d->jobs[foundIndex].jobThread = nullptr; 0605 d->jobs.removeAt(foundIndex); 0606 0607 if (returnedItemInfo.isEmpty()) 0608 { 0609 return; 0610 } 0611 0612 // QElapsedTimer elapsedTimer; 0613 // elapsedTimer.start(); 0614 0615 for (int i = 0 ; i < returnedItemInfo.count() ; ++i) 0616 { 0617 const GPSItemInfo currentItemInfo = returnedItemInfo.at(i); 0618 0619 if (!currentItemInfo.coordinates.hasCoordinates()) 0620 { 0621 continue; 0622 } 0623 0624 if (d->imagesHash.contains(currentItemInfo.id)) 0625 { 0626 continue; 0627 } 0628 0629 d->imagesHash.insert(currentItemInfo.id, currentItemInfo); 0630 0631 const TileIndex markerTileIndex = TileIndex::fromCoordinates(currentItemInfo.coordinates, TileIndex::MaxLevel); 0632 addMarkerToTileAndChildren(currentItemInfo.id, markerTileIndex); 0633 } 0634 0635 // qCDebug(DIGIKAM_GENERAL_LOG) << "added" << returnedItemInfo.count() 0636 // << "markers in" << elapsedTimer.nsecsElapsed() / 1e9 << "seconds"; 0637 0638 Q_EMIT signalTilesOrSelectionChanged(); 0639 } 0640 0641 /** 0642 * @brief Because of a call to pixmapFromRepresentativeIndex, some thumbnails are not yet loaded at the time of requesting. 0643 * When each thumbnail loads, this slot is called and emits a signal that announces the map that the thumbnail is available. 0644 */ 0645 void GPSMarkerTiler::slotThumbnailLoaded(const LoadingDescription& loadingDescription, const QPixmap& thumbnail) 0646 { 0647 QVariant index = d->thumbnailMap.value(loadingDescription.thumbnailIdentifier().id); 0648 /* 0649 QPair<TileIndex, int> indexForPixmap = 0650 index.value<QPair<TileIndex, int> >(); 0651 */ 0652 Q_EMIT signalThumbnailAvailableForIndex(index, thumbnail.copy(1, 1, thumbnail.size().width() - 2, thumbnail.size().height() - 2)); 0653 } 0654 0655 /** 0656 * @brief Sets the map active/inactive 0657 * @param state New state of the map, true means active. 0658 */ 0659 void GPSMarkerTiler::setActive(const bool state) 0660 { 0661 d->activeState = state; 0662 } 0663 0664 AbstractMarkerTiler::Tile* GPSMarkerTiler::tileNew() 0665 { 0666 return new MyTile(); 0667 } 0668 0669 /** 0670 * @brief Receives notifications from the database when images were changed and updates the tiler 0671 */ 0672 void GPSMarkerTiler::slotImageChange(const ImageChangeset& changeset) 0673 { 0674 const DatabaseFields::Set changes = changeset.changes(); 0675 0676 if (!((changes & DatabaseFields::LatitudeNumber) || 0677 (changes & DatabaseFields::LongitudeNumber) || 0678 (changes & DatabaseFields::Altitude))) 0679 { 0680 return; 0681 } 0682 0683 Q_FOREACH (const qlonglong& id, changeset.ids()) 0684 { 0685 const ItemInfo newItemInfo(id); 0686 0687 if (!newItemInfo.hasCoordinates()) 0688 { 0689 if (d->imagesHash.contains(id)) 0690 { 0691 // the image has no coordinates any more 0692 // remove it from the tiles and the image list 0693 0694 const GPSItemInfo oldInfo = d->imagesHash.value(id); 0695 const GeoCoordinates oldCoordinates = oldInfo.coordinates; 0696 const TileIndex oldTileIndex = TileIndex::fromCoordinates(oldCoordinates, TileIndex::MaxLevel); 0697 0698 removeMarkerFromTileAndChildren(id, oldTileIndex); 0699 0700 d->imagesHash.remove(id); 0701 } 0702 0703 continue; 0704 } 0705 0706 GeoCoordinates newCoordinates(newItemInfo.latitudeNumber(), newItemInfo.longitudeNumber()); 0707 0708 if (newItemInfo.hasAltitude()) 0709 { 0710 newCoordinates.setAlt(newItemInfo.altitudeNumber()); 0711 } 0712 0713 if (d->imagesHash.contains(id)) 0714 { 0715 // the image id is known, therefore the image has already been sorted into tiles. 0716 // We assume that the coordinates of the image have changed. 0717 0718 const GPSItemInfo oldInfo = d->imagesHash.value(id); 0719 const GeoCoordinates oldCoordinates = oldInfo.coordinates; 0720 const GPSItemInfo currentItemInfo = GPSItemInfo::fromIdCoordinatesRatingDateTime(id, 0721 newCoordinates, 0722 newItemInfo.rating(), 0723 newItemInfo.dateTime()); 0724 0725 d->imagesHash.insert(id, currentItemInfo); 0726 0727 const TileIndex oldTileIndex = TileIndex::fromCoordinates(oldCoordinates, TileIndex::MaxLevel); 0728 const TileIndex newTileIndex = TileIndex::fromCoordinates(newCoordinates, TileIndex::MaxLevel); 0729 0730 // remove from old position 0731 removeMarkerFromTileAndChildren(id, oldTileIndex); 0732 // add at new position 0733 addMarkerToTileAndChildren(id, newTileIndex); 0734 0735 } 0736 else 0737 { 0738 // the image is new, add it to the existing tiles 0739 0740 const GPSItemInfo currentItemInfo = GPSItemInfo::fromIdCoordinatesRatingDateTime(id, 0741 newCoordinates, 0742 newItemInfo.rating(), 0743 newItemInfo.dateTime()); 0744 0745 d->imagesHash.insert(id, currentItemInfo); 0746 0747 const TileIndex newMarkerTileIndex = TileIndex::fromCoordinates(currentItemInfo.coordinates, TileIndex::MaxLevel); 0748 0749 addMarkerToTileAndChildren(id, newMarkerTileIndex); 0750 } 0751 } 0752 0753 Q_EMIT signalTilesOrSelectionChanged(); 0754 } 0755 0756 /** 0757 * @brief Receives notifications from the album model about new items 0758 */ 0759 void GPSMarkerTiler::slotNewModelData(const QList<ItemInfo>& infoList) 0760 { 0761 // We do not actually store the data from the model, we just want 0762 // to know that something was changed. 0763 /// @todo Also monitor removed, reset, etc. signals 0764 0765 Q_UNUSED(infoList); 0766 0767 Q_EMIT signalTilesOrSelectionChanged(); 0768 } 0769 0770 void GPSMarkerTiler::setRegionSelection(const GeoCoordinates::Pair& sel) 0771 { 0772 d->currentRegionSelection = sel; 0773 0774 if (sel.first.hasCoordinates()) 0775 { 0776 d->mapGlobalGroupState |= RegionSelectedMask; 0777 } 0778 else 0779 { 0780 d->mapGlobalGroupState &= ~RegionSelectedMask; 0781 } 0782 0783 Q_EMIT signalTilesOrSelectionChanged(); 0784 } 0785 0786 void GPSMarkerTiler::removeCurrentRegionSelection() 0787 { 0788 d->currentRegionSelection.first.clear(); 0789 0790 d->mapGlobalGroupState &= ~RegionSelectedMask; 0791 0792 Q_EMIT signalTilesOrSelectionChanged(); 0793 } 0794 0795 void GPSMarkerTiler::onIndicesClicked(const ClickInfo& clickInfo) 0796 { 0797 /// @todo Also handle the representative index 0798 0799 QList<qlonglong> clickedImagesId; 0800 0801 Q_FOREACH (const TileIndex& tileIndex, clickInfo.tileIndicesList) 0802 { 0803 clickedImagesId << getTileMarkerIds(tileIndex); 0804 } 0805 0806 int repImageId = -1; 0807 0808 if (clickInfo.representativeIndex.canConvert<QPair<TileIndex, int> >()) 0809 { 0810 repImageId = clickInfo.representativeIndex.value<QPair<TileIndex, int> >().second; 0811 } 0812 0813 if (clickInfo.currentMouseMode == MouseModeSelectThumbnail && d->selectionModel) 0814 { 0815 /** 0816 * @todo This does not work properly, because not all images in a tile 0817 * may be selectable because some of them are outside of the region selection 0818 */ 0819 const bool doSelect = (clickInfo.groupSelectionState & SelectedMask) != SelectedAll; 0820 0821 const QItemSelectionModel::SelectionFlags selectionFlags = 0822 (doSelect ? QItemSelectionModel::Select : QItemSelectionModel::Deselect) 0823 | QItemSelectionModel::Rows; 0824 0825 for (int i = 0 ; i < clickedImagesId.count() ; ++i) 0826 { 0827 const QModelIndex currentIndex = d->imageFilterModel->indexForImageId(clickedImagesId.at(i)); 0828 0829 if (d->selectionModel->isSelected(currentIndex) != doSelect) 0830 { 0831 d->selectionModel->select(currentIndex, selectionFlags); 0832 } 0833 } 0834 0835 if (repImageId >= 0) 0836 { 0837 const QModelIndex repImageIndex = d->imageFilterModel->indexForImageId(repImageId); 0838 0839 if (repImageIndex.isValid()) 0840 { 0841 d->selectionModel->setCurrentIndex(repImageIndex, selectionFlags); 0842 } 0843 } 0844 } 0845 else if (clickInfo.currentMouseMode == MouseModeFilter) 0846 { 0847 setPositiveFilterIsActive(true); 0848 Q_EMIT signalModelFilteredImages(clickedImagesId); 0849 } 0850 } 0851 0852 QList<qlonglong> GPSMarkerTiler::getTileMarkerIds(const TileIndex& tileIndex) 0853 { 0854 Q_ASSERT(tileIndex.level() <= TileIndex::MaxLevel); 0855 0856 const MyTile* const myTile = static_cast<MyTile*>(getTile(tileIndex, true)); 0857 0858 if (!myTile) 0859 { 0860 return QList<qlonglong>(); 0861 } 0862 0863 return myTile->imagesId; 0864 } 0865 0866 GeoGroupState GPSMarkerTiler::getGlobalGroupState() 0867 { 0868 return d->mapGlobalGroupState; 0869 } 0870 0871 GeoGroupState GPSMarkerTiler::getImageState(const qlonglong imageId) 0872 { 0873 GeoGroupState imageState; 0874 0875 // is the image inside the region selection? 0876 0877 if (d->mapGlobalGroupState & RegionSelectedMask) 0878 { 0879 const QModelIndex imageAlbumModelIndex = d->imageAlbumModel->indexForImageId(imageId); 0880 0881 if (imageAlbumModelIndex.isValid()) 0882 { 0883 imageState |= RegionSelectedAll; 0884 } 0885 else 0886 { 0887 // not inside region selection, therefore 0888 // no other flags can apply 0889 0890 return RegionSelectedNone; 0891 } 0892 } 0893 0894 // is the image positively filtered? 0895 0896 if (d->mapGlobalGroupState & FilteredPositiveMask) 0897 { 0898 const QModelIndex imageIndexInFilterModel = d->imageFilterModel->indexForImageId(imageId); 0899 0900 if (imageIndexInFilterModel.isValid()) 0901 { 0902 imageState |= FilteredPositiveAll; 0903 0904 // is the image selected? 0905 0906 if (d->selectionModel->hasSelection()) 0907 { 0908 if (d->selectionModel->isSelected(imageIndexInFilterModel)) 0909 { 0910 imageState |= SelectedAll; 0911 } 0912 } 0913 } 0914 else 0915 { 0916 // the image is not positively filtered, therefore it can 0917 // not be selected 0918 0919 return imageState; 0920 } 0921 } 0922 else 0923 { 0924 // is the image selected? 0925 0926 if (d->selectionModel->hasSelection()) 0927 { 0928 const QModelIndex imageIndexInFilterModel = d->imageFilterModel->indexForImageId(imageId); 0929 0930 if (d->selectionModel->isSelected(imageIndexInFilterModel)) 0931 { 0932 imageState |= SelectedAll; 0933 } 0934 } 0935 } 0936 0937 return imageState; 0938 } 0939 0940 void GPSMarkerTiler::setPositiveFilterIsActive(const bool state) 0941 { 0942 if (state) 0943 { 0944 d->mapGlobalGroupState |= FilteredPositiveMask; 0945 } 0946 else 0947 { 0948 d->mapGlobalGroupState &= ~FilteredPositiveMask; 0949 } 0950 0951 /// @todo Somehow, a delay is necessary before emitting this signal - probably the order in which the filtering 0952 /// is propagated to other parts of digikam is wrong or just takes too long 0953 0954 QTimer::singleShot(100, this, SIGNAL(signalTilesOrSelectionChanged())); 0955 /* 0956 Q_EMIT signalTilesOrSelectionChanged(); 0957 */ 0958 } 0959 0960 void GPSMarkerTiler::slotSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected) 0961 { 0962 /// @todo Buffer this information, update the tiles, etc. 0963 0964 Q_UNUSED(selected); 0965 Q_UNUSED(deselected); 0966 0967 Q_EMIT signalTilesOrSelectionChanged(); 0968 } 0969 0970 void GPSMarkerTiler::removeMarkerFromTileAndChildren(const qlonglong imageId, const TileIndex& markerTileIndex) 0971 { 0972 MyTile* currentParentTile = nullptr; 0973 MyTile* currentTile = static_cast<MyTile*>(rootTile()); 0974 0975 for (int level = 0 ; level <= markerTileIndex.level() ; ++level) 0976 { 0977 currentTile->imagesId.removeOne(imageId); 0978 0979 if (currentTile->imagesId.isEmpty()) 0980 { 0981 if (currentTile == rootTile()) 0982 { 0983 break; 0984 } 0985 0986 // this tile can be deleted 0987 0988 if (currentParentTile) 0989 { 0990 currentParentTile->deleteChild(currentTile); 0991 } 0992 0993 break; 0994 } 0995 0996 currentParentTile = currentTile; 0997 currentTile = static_cast<MyTile*>(currentParentTile->getChild(markerTileIndex.at(level))); 0998 0999 if (!currentTile) 1000 { 1001 break; 1002 } 1003 } 1004 } 1005 1006 void GPSMarkerTiler::addMarkerToTileAndChildren(const qlonglong imageId, const TileIndex& markerTileIndex) 1007 { 1008 MyTile* currentTile = static_cast<MyTile*>(rootTile()); 1009 1010 for (int level = 0 ; level <= markerTileIndex.level() ; ++level) 1011 { 1012 currentTile->imagesId.append(imageId); 1013 1014 if (currentTile->childrenEmpty()) 1015 { 1016 break; 1017 } 1018 1019 MyTile* nextTile = static_cast<MyTile*>(currentTile->getChild(markerTileIndex.at(level))); 1020 1021 if (!nextTile) 1022 { 1023 nextTile = static_cast<MyTile*>(tileNew()); 1024 currentTile->addChild(markerTileIndex.at(level), nextTile); 1025 } 1026 1027 currentTile = nextTile; 1028 } 1029 } 1030 1031 } // namespace Digikam 1032 1033 #include "moc_gpsmarkertiler.cpp"