File indexing completed on 2025-01-19 03:58:09

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2010-12-05
0007  * Description : Merges tiles into groups
0008  *
0009  * SPDX-FileCopyrightText: 2010-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0010  * SPDX-FileCopyrightText: 2010-2011 by Michael G. Hansen <mike at mghansen dot de>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "tilegrouper.h"
0017 
0018 // Qt includes
0019 
0020 #include <QElapsedTimer>
0021 
0022 // C++ includes
0023 
0024 #include <cmath>
0025 
0026 // local includes
0027 
0028 #include "abstractmarkertiler.h"
0029 #include "mapbackend.h"
0030 #include "digikam_debug.h"
0031 
0032 namespace Digikam
0033 {
0034 
0035 class Q_DECL_HIDDEN TileGrouper::Private
0036 {
0037 public:
0038 
0039     explicit Private()
0040         : clustersDirty(true),
0041           currentBackend(nullptr)
0042     {
0043     }
0044 
0045     bool        clustersDirty;
0046     MapBackend* currentBackend;
0047 };
0048 
0049 TileGrouper::TileGrouper(const QExplicitlySharedDataPointer<GeoIfaceSharedData>& sharedData,
0050                          QObject* const parent)
0051     : QObject(parent),
0052       d(new Private),
0053       s(sharedData)
0054 {
0055     qRegisterMetaType<QVector<int>>("QVector<int>");
0056 }
0057 
0058 TileGrouper::~TileGrouper()
0059 {
0060 }
0061 
0062 void TileGrouper::setClustersDirty()
0063 {
0064     d->clustersDirty = true;
0065 }
0066 
0067 bool TileGrouper::getClustersDirty() const
0068 {
0069     return d->clustersDirty;
0070 }
0071 
0072 void TileGrouper::setCurrentBackend(MapBackend* const backend)
0073 {
0074     d->currentBackend = backend;
0075 }
0076 
0077 bool TileGrouper::currentBackendReady()
0078 {
0079     if (!d->currentBackend)
0080     {
0081         return false;
0082     }
0083 
0084     return d->currentBackend->isReady();
0085 }
0086 
0087 void TileGrouper::updateClusters()
0088 {
0089     if (!s->markerModel)
0090     {
0091         return;
0092     }
0093 
0094     if (s->haveMovingCluster)
0095     {
0096         // do not re-cluster while a cluster is being moved
0097 
0098         return;
0099     }
0100 
0101     if (!currentBackendReady())
0102     {
0103         return;
0104     }
0105 
0106     if (!d->clustersDirty)
0107     {
0108        return;
0109     }
0110 
0111     d->clustersDirty = false;
0112 
0113     // constants for clusters
0114 
0115     const int ClusterRadius          = s->showThumbnails ? s->thumbnailGroupingRadius : s->markerGroupingRadius;
0116 /*
0117     const QSize ClusterDefaultSize   = QSize(2*ClusterRadius, 2*ClusterRadius);
0118 */
0119     const int ClusterGridSizeScreen  = 4 * ClusterRadius;
0120 /*
0121     const QSize ClusterMaxPixmapSize = QSize(ClusterGridSizeScreen, ClusterGridSizeScreen);
0122 */
0123 //     qCDebug(DIGIKAM_GEOIFACE_LOG)<<"updateClusters starting...";
0124 
0125     s->clusterList.clear();
0126 
0127     const int markerLevel                                   = d->currentBackend->getMarkerModelLevel();
0128     QList<QPair<GeoCoordinates, GeoCoordinates> > mapBounds = d->currentBackend->getNormalizedBounds();
0129 /*
0130     // debug output for tile level diagnostics:
0131 
0132     QIntList tile1;
0133     tile1<<520;
0134     QIntList tile2 = tile1;
0135 
0136     for (int i = 1; i <= s->markerModel->maxLevel()-1; ++i)
0137     {
0138         tile2 = tile1;
0139         tile2<<0;
0140         tile1<<1;
0141         const GeoCoordinates tile1Coordinate = s->markerModel->tileIndexToCoordinate(tile1);
0142         const GeoCoordinates tile2Coordinate = s->markerModel->tileIndexToCoordinate(tile2);
0143         QPoint tile1Point, tile2Point;
0144         d->currentBackend->screenCoordinates(tile1Coordinate, &tile1Point);
0145         d->currentBackend->screenCoordinates(tile2Coordinate, &tile2Point);
0146         qCDebug(DIGIKAM_GEOIFACE_LOG)<<i<<tile1Point<<tile2Point<<(tile1Point-tile2Point);
0147     }
0148 */
0149     const int gridSize   = ClusterGridSizeScreen;
0150     const QSize mapSize  = d->currentBackend->mapSize();
0151     const int gridWidth  = mapSize.width();
0152     const int gridHeight = mapSize.height();
0153     QVector<QList<TileIndex> > pixelNonEmptyTileIndexGrid(gridWidth*gridHeight, QList<TileIndex>());
0154     QVector<int> pixelCountGrid(gridWidth*gridHeight, 0);
0155     QList<QPair<QPoint, QPair<int, QList<TileIndex> > > > leftOverList;
0156 
0157     /// @todo Iterate only over the visible part of the map
0158 
0159     int debugCountNonEmptyTiles = 0;
0160     int debugTilesSearched      = 0;
0161 
0162     /// @todo Review this
0163 
0164     for (int i = 0 ; i < mapBounds.count() ; ++i)
0165     {
0166         s->markerModel->prepareTiles(mapBounds.at(i).first, mapBounds.at(i).second, markerLevel);
0167     }
0168 
0169     QElapsedTimer elapsedTimer;
0170     elapsedTimer.start();
0171 
0172     for (AbstractMarkerTiler::NonEmptyIterator tileIterator(s->markerModel, markerLevel, mapBounds) ;
0173          !tileIterator.atEnd() ; tileIterator.nextIndex())
0174     {
0175         const TileIndex tileIndex           = tileIterator.currentIndex();
0176 
0177         // find out where the tile is on the map:
0178 
0179         const GeoCoordinates tileCoordinate = tileIndex.toCoordinates();
0180         debugTilesSearched++;
0181         QPoint tilePoint;
0182 
0183         if (!d->currentBackend->screenCoordinates(tileCoordinate, &tilePoint))
0184         {
0185             continue;
0186         }
0187 
0188         // make sure we are in the grid (in case there are rounding errors somewhere in the backend
0189 
0190         if ((tilePoint.x() < 0) || (tilePoint.y() < 0) || (tilePoint.x() >= gridWidth) || (tilePoint.y() >= gridHeight))
0191         {
0192             continue;
0193         }
0194 
0195         debugCountNonEmptyTiles++;
0196         const int linearIndex        = tilePoint.x() + tilePoint.y()*gridWidth;
0197         pixelNonEmptyTileIndexGrid[linearIndex] << tileIndex;
0198         pixelCountGrid[linearIndex] += s->markerModel->getTileMarkerCount(tileIndex);
0199 /*
0200         qCDebug(DIGIKAM_GEOIFACE_LOG) << QString::fromLatin1("pixel at: %1, %2 (%3): %4 markers")
0201                                          .arg(tilePoint.x())
0202                                          .arg(tilePoint.y()).arg(linearIndex)
0203                                          .arg(pixelCountGrid[linearIndex]);
0204 */
0205     }
0206 
0207     qCDebug(DIGIKAM_GEOIFACE_LOG) << "iterating over " << debugCountNonEmptyTiles
0208                                   << " took " << elapsedTimer.nsecsElapsed() / 1e9 << " seconds";
0209 
0210     /// @todo Cleanup this list every ... iterations in the next loop, too
0211 
0212     QIntList nonEmptyPixelIndices;
0213 
0214     for (int i = 0 ; i < gridWidth*gridHeight ; ++i)
0215     {
0216         if (pixelCountGrid.at(i) > 0)
0217         {
0218             nonEmptyPixelIndices << i;
0219         }
0220     }
0221 
0222     // re-add the markers to clusters:
0223 /*
0224     int lastTooCloseClusterIndex = 0;
0225 */
0226 
0227     Q_FOREVER
0228     {
0229         // here we store candidates for clusters:
0230 
0231         int markerMax             = 0;
0232         int markerX               = 0;
0233         int markerY               = 0;
0234         int pixelGridMetaIndexMax = 0;
0235 
0236         for (int pixelGridMetaIndex = 0 ; pixelGridMetaIndex < nonEmptyPixelIndices.size() ; ++pixelGridMetaIndex)
0237         {
0238             const int index = nonEmptyPixelIndices.at(pixelGridMetaIndex);
0239 
0240             if (index < 0)
0241             {
0242                 continue;
0243             }
0244 
0245             if (pixelCountGrid.at(index) == 0)
0246             {
0247                 /// @todo Also remove this entry from the list to speed up the loop!
0248 
0249                 nonEmptyPixelIndices[pixelGridMetaIndex] = -1;
0250                 continue;
0251             }
0252 
0253             if (pixelCountGrid.at(index) > markerMax)
0254             {
0255                 // calculate x,y from the linear index:
0256 
0257                 const int x = index % gridWidth;
0258                 const int y = (index-x)/gridWidth;
0259                 const QPoint markerPosition(x, y);
0260 
0261                 // only use this as a candidate for a cluster if it is not too close to another cluster:
0262 
0263                 bool tooClose = false;
0264 
0265                 /// @todo Check the cluster that was a problem last time first:
0266 /*
0267                 if (lastTooCloseClusterIndex<s->clusterList.size())
0268                 {
0269                     tooClose = QPointSquareDistance(s->clusterList.at(lastTooCloseClusterIndex).pixelPos, markerPosition) < pow(ClusterGridSizeScreen/2, 2);
0270                 }
0271 */
0272                 // now check all other clusters:
0273 
0274                 for (int i = 0 ; (!tooClose) && (i < s->clusterList.size()) ; ++i)
0275                 {
0276                     if (i == index)
0277                     {
0278                         continue;
0279                     }
0280 
0281                     tooClose = QPointSquareDistance(s->clusterList.at(i).pixelPos, markerPosition) < pow(ClusterGridSizeScreen/2, 2);
0282 /*
0283                     if (tooClose)
0284                     {
0285                         lastTooCloseClusterIndex = i;
0286                     }
0287 */
0288                 }
0289 
0290                 if (tooClose)
0291                 {
0292                     // move markers into leftover list
0293 
0294                     leftOverList << QPair<QPoint, QPair<int, QList<TileIndex> > >(QPoint(x,y),
0295                                     QPair<int, QList<TileIndex> >(pixelCountGrid.at(index), pixelNonEmptyTileIndexGrid.at(index)));
0296                     pixelCountGrid[index]                    = 0;
0297                     pixelNonEmptyTileIndexGrid[index].clear();
0298                     nonEmptyPixelIndices[pixelGridMetaIndex] = -1;
0299                 }
0300                 else
0301                 {
0302                     markerMax             = pixelCountGrid.at(index);
0303                     markerX               = x;
0304                     markerY               = y;
0305                     pixelGridMetaIndexMax = pixelGridMetaIndex;
0306                 }
0307             }
0308         }
0309 
0310         if (markerMax == 0)
0311         {
0312             break;
0313         }
0314 
0315         GeoCoordinates clusterCoordinates = pixelNonEmptyTileIndexGrid.at(markerX+markerY*gridWidth).first().toCoordinates();
0316         GeoIfaceCluster cluster;
0317         cluster.coordinates               = clusterCoordinates;
0318         cluster.pixelPos                  = QPoint(markerX, markerY);
0319         cluster.tileIndicesList           = pixelNonEmptyTileIndexGrid.at(markerX+markerY*gridWidth);
0320         cluster.markerCount               = pixelCountGrid.at(markerX+markerY*gridWidth);
0321 
0322         // mark the pixel as done:
0323 
0324         pixelCountGrid[markerX+markerY*gridWidth]   = 0;
0325         pixelNonEmptyTileIndexGrid[markerX+markerY*gridWidth].clear();
0326         nonEmptyPixelIndices[pixelGridMetaIndexMax] = -1;
0327 
0328         // absorb all markers around it:
0329         // Now we only remove the markers from the pixelgrid. They will be cleared from the
0330         // pixelGridIndices in the loop above
0331         // make sure we do not go over the grid boundaries:
0332 
0333         const int eatRadius = gridSize/4;
0334         const int xStart    = qMax( (markerX-eatRadius), 0);
0335         const int yStart    = qMax( (markerY-eatRadius), 0);
0336         const int xEnd      = qMin( (markerX+eatRadius), gridWidth-1);
0337         const int yEnd      = qMin( (markerY+eatRadius), gridHeight-1);
0338 
0339         for (int indexX = xStart ; indexX <= xEnd ; ++indexX)
0340         {
0341             for (int indexY = yStart ; indexY <= yEnd ; ++indexY)
0342             {
0343                 const int index       = indexX + indexY*gridWidth;
0344                 cluster.tileIndicesList << pixelNonEmptyTileIndexGrid.at(index);
0345                 pixelNonEmptyTileIndexGrid[index].clear();
0346                 cluster.markerCount  += pixelCountGrid.at(index);
0347                 pixelCountGrid[index] = 0;
0348             }
0349         }
0350 
0351         qCDebug(DIGIKAM_GEOIFACE_LOG) << QString::fromLatin1("created cluster %1: %2 tiles")
0352                                          .arg(s->clusterList.size())
0353                                          .arg(cluster.tileIndicesList.count());
0354 
0355         s->clusterList << cluster;
0356     }
0357 
0358     // now move all leftover markers into clusters:
0359 
0360     for (QList<QPair<QPoint, QPair<int, QList<TileIndex> > > >::const_iterator it = leftOverList.constBegin();
0361          it != leftOverList.constEnd() ; ++it)
0362     {
0363         const QPoint markerPosition = it->first;
0364 
0365         // find the closest cluster:
0366 
0367         int closestSquareDistance   = 0;
0368         int closestIndex            = -1;
0369 
0370         for (int i = 0 ; i < s->clusterList.size() ; ++i)
0371         {
0372             const int squareDistance = QPointSquareDistance(s->clusterList.at(i).pixelPos, markerPosition);
0373 
0374             if ((closestIndex < 0) || (squareDistance < closestSquareDistance))
0375             {
0376                 closestSquareDistance = squareDistance;
0377                 closestIndex          = i;
0378             }
0379         }
0380 
0381         if (closestIndex >= 0)
0382         {
0383             s->clusterList[closestIndex].markerCount += it->second.first;
0384             s->clusterList[closestIndex].tileIndicesList << it->second.second;
0385         }
0386     }
0387 
0388     // determine the selected states of the clusters:
0389 
0390     for (int i = 0 ; i < s->clusterList.count() ; ++i)
0391     {
0392         GeoIfaceCluster& cluster = s->clusterList[i];
0393         int clusterSelectedCount = 0;
0394         GroupStateComputer clusterStateComputer;
0395 
0396         for (int iTile = 0 ; (iTile < cluster.tileIndicesList.count()) ; ++iTile)
0397         {
0398             const TileIndex tileIndex              = cluster.tileIndicesList.at(iTile);
0399             const GeoGroupState tileGroupState     = s->markerModel->getTileGroupState(tileIndex);
0400             clusterStateComputer.addState(tileGroupState);
0401             clusterSelectedCount                  += s->markerModel->getTileSelectedCount(tileIndex);
0402         }
0403 
0404         cluster.markerSelectedCount = clusterSelectedCount;
0405         cluster.groupState          = clusterStateComputer.getState();
0406     }
0407 
0408 //     qCDebug(DIGIKAM_GEOIFACE_LOG) << s->clusterList.size();
0409 
0410     qCDebug(DIGIKAM_GEOIFACE_LOG) << QString::fromLatin1("level %1: %2 non empty tiles sorted into %3 clusters (%4 searched)")
0411                                      .arg(markerLevel)
0412                                      .arg(debugCountNonEmptyTiles)
0413                                      .arg(s->clusterList.count())
0414                                      .arg(debugTilesSearched);
0415 
0416     d->currentBackend->updateClusters();
0417 }
0418 
0419 } // namespace Digikam
0420 
0421 #include "moc_tilegrouper.cpp"