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"