1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2010-07-20
7  * Description : GPS search marker tiler
8  *
9  * Copyright (C) 2010      by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
10  * Copyright (C) 2010      by Gabriel Voicu <ping dot gabi at gmail dot com>
11  * Copyright (C) 2010-2011 by Michael G. Hansen <mike at mghansen dot de>
12  * Copyright (C) 2015      by Mohamed_Anwer <m_dot_anwer at gmx dot com>
13  *
14  * This program is free software; you can redistribute it
15  * and/or modify it under the terms of the GNU General
16  * Public License as published by the Free Software Foundation;
17  * either version 2, or (at your option)
18  * any later version.
19  *
20  * This program is distributed in the hope that it will be useful,
21  * but WITHOUT ANY WARRANTY; without even the implied warranty of
22  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23  * GNU General Public License for more details.
24  *
25  * ============================================================ */
26 
27 #include "gpsmarkertiler.h"
28 
29 // Qt includes
30 
31 #include <QPair>
32 #include <QRectF>
33 #include <QTimer>
34 
35 // Local includes
36 
37 #include "groupstatecomputer.h"
38 #include "gpsiteminfosorter.h"
39 #include "dnotificationwrapper.h"
40 #include "digikamapp.h"
41 #include "digikam_debug.h"
42 #include "dbjobsmanager.h"
43 
44 /// @todo Actually use this definition!
45 typedef QPair<Digikam::TileIndex, int> MapPair;
46 
47 Q_DECLARE_METATYPE(MapPair)
48 
49 namespace Digikam
50 {
51 
52 /**
53  * @class GPSMarkerTiler
54  *
55  * @brief Marker model for storing data needed to display markers on the map. The data is retrieved from Digikam's database.
56  */
57 
58 class Q_DECL_HIDDEN GPSMarkerTiler::MyTile : public Tile
59 {
60 public:
61 
62     QList<qlonglong> imagesId;
63 };
64 
65 class Q_DECL_HIDDEN GPSMarkerTiler::Private
66 {
67 public:
68 
69     class Q_DECL_HIDDEN InternalJobs
70     {
71     public:
72 
InternalJobs()73         InternalJobs()
74             : level           (0),
75               jobThread       (nullptr),
76               dataFromDatabase()
77         {
78         }
79 
80         int                level;
81         GPSDBJobsThread*   jobThread;
82         QList<GPSItemInfo> dataFromDatabase;
83     };
84 
Private()85     explicit Private()
86         : jobs                  (),
87           thumbnailLoadThread   (nullptr),
88           thumbnailMap          (),
89           rectList              (),
90           activeState           (true),
91           imagesHash            (),
92           imageFilterModel      (),
93           imageAlbumModel       (),
94           selectionModel        (),
95           currentRegionSelection(),
96           mapGlobalGroupState   ()
97     {
98     }
99 
100     QList<InternalJobs>           jobs;
101     ThumbnailLoadThread*          thumbnailLoadThread;
102     QHash<qlonglong, QVariant>    thumbnailMap;
103     QList<QRectF>                 rectList;
104     bool                          activeState;
105     QHash<qlonglong, GPSItemInfo> imagesHash;
106     ItemFilterModel*              imageFilterModel;
107     ItemAlbumModel*               imageAlbumModel;
108     QItemSelectionModel*          selectionModel;
109     GeoCoordinates::Pair          currentRegionSelection;
110     GeoGroupState                 mapGlobalGroupState;
111 };
112 
113 /**
114  * @brief Constructor
115  * @param parent the parent object
116  */
GPSMarkerTiler(QObject * const parent,ItemFilterModel * const imageFilterModel,QItemSelectionModel * const selectionModel)117 GPSMarkerTiler::GPSMarkerTiler(QObject* const parent,
118                                ItemFilterModel* const imageFilterModel,
119                                QItemSelectionModel* const selectionModel)
120     : AbstractMarkerTiler(parent),
121       d                  (new Private())
122 {
123     resetRootTile();
124 
125     d->thumbnailLoadThread = new ThumbnailLoadThread(this);
126     d->imageFilterModel    = imageFilterModel;
127     d->imageAlbumModel     = qobject_cast<ItemAlbumModel*>(imageFilterModel->sourceModel());
128     d->selectionModel      = selectionModel;
129 
130     connect(d->thumbnailLoadThread, SIGNAL(signalThumbnailLoaded(LoadingDescription,QPixmap)),
131             this, SLOT(slotThumbnailLoaded(LoadingDescription,QPixmap)));
132 
133     connect(CoreDbAccess::databaseWatch(), SIGNAL(imageChange(ImageChangeset)),
134             this, SLOT(slotImageChange(ImageChangeset)), Qt::QueuedConnection);
135 
136     connect(d->imageAlbumModel, SIGNAL(imageInfosAdded(QList<ItemInfo>)),
137             this, SLOT(slotNewModelData(QList<ItemInfo>)));
138 
139     connect(d->selectionModel, SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
140             this, SLOT(slotSelectionChanged(QItemSelection,QItemSelection)));
141 }
142 
143 /**
144  * @brief Destructor
145  */
~GPSMarkerTiler()146 GPSMarkerTiler::~GPSMarkerTiler()
147 {
148     delete d;
149 }
150 
regenerateTiles()151 void GPSMarkerTiler::regenerateTiles()
152 {
153 }
154 
155 /**
156  * @brief Requests all images inside a given rectangle from the database.
157  *
158  * This function calls the database for the images found inside a rectangle
159  * defined by upperLeft and lowerRight points. The images are returned from
160  * the database in batches.
161  *
162  * @param upperLeft The North-West point.
163  * @param lowerRight The South-East point.
164  * @param level The requested tiling level.
165  */
prepareTiles(const GeoCoordinates & upperLeft,const GeoCoordinates & lowerRight,int level)166 void GPSMarkerTiler::prepareTiles(const GeoCoordinates& upperLeft, const GeoCoordinates& lowerRight, int level)
167 {
168     const QRectF worldRect(-90,-180,180,360);
169 
170     qreal lat1         = upperLeft.lat();
171     qreal lng1         = upperLeft.lon();
172     qreal lat2         = lowerRight.lat();
173     qreal lng2         = lowerRight.lon();
174     auto requestedRect = worldRect.intersected(QRectF(lat1, lng1, lat2 - lat1, lng2 - lng1));
175 
176     for (int i = 0 ; i < d->rectList.count() ; ++i)
177     {
178         // is there a rect that contains the requested one?
179         const QRectF& currentRect = d->rectList.at(i);
180 
181         if (currentRect.contains(requestedRect))
182         {
183             return;
184         }
185 
186         // and remove rects that are contained in the requested one
187 
188         if (requestedRect.contains(currentRect))
189         {
190             std::swap(d->rectList[i], d->rectList.back());
191             d->rectList.removeLast();
192             // we removed one entry. we have to subtract one from the index
193             --i;
194         }
195     }
196 
197     // grow the rect a bit such that we don't have to request many small ones while panning
198     qreal marginW = requestedRect.width() * 0.05;
199     qreal marginH = requestedRect.height() * 0.05;
200     requestedRect = requestedRect.marginsAdded(QMarginsF(marginW, marginH, marginW, marginH));
201     requestedRect = worldRect.intersected(requestedRect);
202     requestedRect.getCoords(&lat1, &lng1, &lat2, &lng2);
203 
204     for (int i = 0 ; i < d->rectList.count() ; ++i)
205     {
206         qreal rectLat1, rectLng1, rectLat2, rectLng2;
207         const QRectF currentRect = d->rectList.at(i);
208         currentRect.getCoords(&rectLat1, &rectLng1, &rectLat2, &rectLng2);
209 
210         if      (currentRect.contains(lat1, lng1))
211         {
212             if (currentRect.contains(lat2, lng1))
213             {
214                 lng1 = rectLng2;
215                 break;
216             }
217         }
218         else if (currentRect.contains(lat2, lng1))
219         {
220             if (currentRect.contains(lat2, lng2))
221             {
222                 lat2 = rectLat1;
223                 break;
224             }
225         }
226         else if (currentRect.contains(lat2, lng2))
227         {
228             if (currentRect.contains(lat1, lng2))
229             {
230                 lng2 = rectLng1;
231                 break;
232             }
233         }
234         else if (currentRect.contains(lat1, lng2))
235         {
236             if (currentRect.contains(lat1, lng1))
237             {
238                 lat1 = rectLat2;
239                 break;
240             }
241         }
242     }
243 
244     requestedRect = QRectF(lat1, lng1, lat2 - lat1, lng2 - lng1);
245     d->rectList.append(requestedRect);
246 
247     qCDebug(DIGIKAM_GENERAL_LOG) << "Listing" << lat1 << lat2 << lng1 << lng2;
248 
249     GPSDBJobInfo jobInfo;
250     jobInfo.setLat1(lat1);
251     jobInfo.setLat2(lat2);
252     jobInfo.setLng1(lng1);
253     jobInfo.setLng2(lng2);
254 
255     GPSDBJobsThread* const currentJob = DBJobsManager::instance()->startGPSJobThread(jobInfo);
256 
257     Private::InternalJobs currentJobInfo;
258 
259     currentJobInfo.jobThread          = currentJob;
260     currentJobInfo.level              = level;
261 
262     d->jobs.append(currentJobInfo);
263 
264     connect(currentJob, SIGNAL(finished()),
265             this, SLOT(slotMapImagesJobResult()));
266 
267     connect(currentJob, SIGNAL(data(QList<ItemListerRecord>)),
268             this, SLOT(slotMapImagesJobData(QList<ItemListerRecord>)));
269 }
270 
271 /**
272  * @brief Returns a pointer to a tile.
273  * @param tileIndex The index of a tile.
274  * @param stopIfEmpty Determines whether child tiles are also created for empty tiles.
275  */
getTile(const TileIndex & tileIndex,const bool stopIfEmpty)276 AbstractMarkerTiler::Tile* GPSMarkerTiler::getTile(const TileIndex& tileIndex, const bool stopIfEmpty)
277 {
278     Q_ASSERT(tileIndex.level() <= TileIndex::MaxLevel);
279 
280     MyTile* tile = static_cast<MyTile*>(rootTile());
281 
282     for (int level = 0 ; level < tileIndex.indexCount() ; ++level)
283     {
284         const int currentIndex = tileIndex.linearIndex(level);
285         MyTile* childTile      = nullptr;
286 
287         if (tile->childrenEmpty())
288         {
289             for (int i = 0 ; i < tile->imagesId.count() ; ++i)
290             {
291                 const int currentImageId          = tile->imagesId.at(i);
292                 const GPSItemInfo currentItemInfo = d->imagesHash[currentImageId];
293                 const TileIndex markerTileIndex   = TileIndex::fromCoordinates(currentItemInfo.coordinates, level);
294                 const int newTileIndex            = markerTileIndex.lastIndex();
295                 MyTile* const newTile1            = static_cast<MyTile*>(tile->getChild(newTileIndex));
296 
297                 if (newTile1 == nullptr)
298                 {
299                     MyTile* const newTile2 = static_cast<MyTile*>(tileNew());
300                     newTile2->imagesId.append(currentImageId);
301                     tile->addChild(newTileIndex, newTile2);
302                 }
303                 else
304                 {
305                     if (!newTile1->imagesId.contains(currentImageId))
306                     {
307                         newTile1->imagesId.append(currentImageId);
308                     }
309                 }
310             }
311         }
312 
313         childTile = static_cast<MyTile*>(tile->getChild(currentIndex));
314 
315         if (childTile == nullptr)
316         {
317             if (stopIfEmpty)
318             {
319                 // there will be no markers in this tile, therefore stop
320 
321                 return nullptr;
322             }
323 
324             childTile = static_cast<MyTile*>(tileNew());
325             tile->addChild(currentIndex, childTile);
326         }
327 
328         tile = childTile;
329     }
330 
331     return tile;
332 }
333 
getTileMarkerCount(const TileIndex & tileIndex)334 int GPSMarkerTiler::getTileMarkerCount(const TileIndex& tileIndex)
335 {
336     MyTile* const tile = static_cast<MyTile*>(getTile(tileIndex, true));
337 
338     if (tile)
339     {
340         return tile->imagesId.count();
341     }
342 
343     return 0;
344 }
345 
getTileSelectedCount(const TileIndex & tileIndex)346 int GPSMarkerTiler::getTileSelectedCount(const TileIndex& tileIndex)
347 {
348     Q_UNUSED(tileIndex)
349 
350     return 0;
351 }
352 
353 /**
354  * @brief This function finds the best representative marker from a tile of markers.
355  * @param tileIndex Index of the tile from which the best marker should be found.
356  * @param sortKey Sets the criteria for selecting the representative thumbnail, a combination of the SortOptions bits.
357  * @return Returns the internally used index of the marker.
358  */
getTileRepresentativeMarker(const TileIndex & tileIndex,const int sortKey)359 QVariant GPSMarkerTiler::getTileRepresentativeMarker(const TileIndex& tileIndex, const int sortKey)
360 {
361     MyTile* const tile = static_cast<MyTile*>(getTile(tileIndex, true));
362 
363     if (!tile)
364     {
365         return QVariant();
366     }
367 
368     if (tile->imagesId.isEmpty())
369     {
370         return QVariant();
371     }
372 
373     GPSItemInfo bestMarkerInfo         = d->imagesHash.value(tile->imagesId.first());
374     GeoGroupState bestMarkerGroupState = getImageState(bestMarkerInfo.id);
375 
376     for (int i = 1 ; i < tile->imagesId.count() ; ++i)
377     {
378         const GPSItemInfo currentMarkerInfo         = d->imagesHash.value(tile->imagesId.at(i));
379         const GeoGroupState currentMarkerGroupState = getImageState(currentMarkerInfo.id);
380 
381         if (GPSItemInfoSorter::fitsBetter(bestMarkerInfo,
382                                           bestMarkerGroupState,
383                                           currentMarkerInfo,
384                                           currentMarkerGroupState,
385                                           getGlobalGroupState(),
386                                           GPSItemInfoSorter::SortOptions(sortKey)))
387         {
388             bestMarkerInfo       = currentMarkerInfo;
389             bestMarkerGroupState = currentMarkerGroupState;
390         }
391     }
392 
393     const QPair<TileIndex, int> returnedMarker(tileIndex, bestMarkerInfo.id);
394 
395     return QVariant::fromValue(returnedMarker);
396 }
397 
398 /**
399  * @brief This function finds the best representative marker from a group of markers. This is needed to display a thumbnail for a marker group.
400  * @param indices A list containing markers, obtained by getTileRepresentativeMarker.
401  * @param sortKey Sets the criteria for selecting the representative thumbnail, a combination of the SortOptions bits.
402  * @return Returns the internally used index of the marker.
403  */
bestRepresentativeIndexFromList(const QList<QVariant> & indices,const int sortKey)404 QVariant GPSMarkerTiler::bestRepresentativeIndexFromList(const QList<QVariant>& indices, const int sortKey)
405 {
406     if (indices.isEmpty())
407     {
408         return QVariant();
409     }
410 
411     const QPair<TileIndex, int> firstIndex = indices.first().value<QPair<TileIndex, int> >();
412     GPSItemInfo bestMarkerInfo             = d->imagesHash.value(firstIndex.second);
413     GeoGroupState bestMarkerGroupState     = getImageState(firstIndex.second);
414     TileIndex bestMarkerTileIndex          = firstIndex.first;
415 
416     for (int i = 1 ; i < indices.count() ; ++i)
417     {
418         const QPair<TileIndex, int> currentIndex = indices.at(i).value<QPair<TileIndex, int> >();
419 
420         GPSItemInfo currentMarkerInfo            = d->imagesHash.value(currentIndex.second);
421         GeoGroupState currentMarkerGroupState    = getImageState(currentIndex.second);
422 
423         if (GPSItemInfoSorter::fitsBetter(bestMarkerInfo,
424                                           bestMarkerGroupState,
425                                           currentMarkerInfo,
426                                           currentMarkerGroupState,
427                                           getGlobalGroupState(),
428                                           GPSItemInfoSorter::SortOptions(sortKey)))
429         {
430             bestMarkerInfo       = currentMarkerInfo;
431             bestMarkerGroupState = currentMarkerGroupState;
432             bestMarkerTileIndex  = currentIndex.first;
433         }
434     }
435 
436     const QPair<TileIndex, int> returnedMarker(bestMarkerTileIndex, bestMarkerInfo.id);
437 
438     return QVariant::fromValue(returnedMarker);
439 }
440 
441 /**
442  * @brief This function retrieves the thumbnail for an index.
443  * @param index The marker's index.
444  * @param size The size of the thumbnail.
445  * @return If the thumbnail has been loaded in the ThumbnailLoadThread instance, it is returned.
446  * If not, a QPixmap is returned and ThumbnailLoadThread's signal named signalThumbnailLoaded is emitted when the thumbnail becomes available.
447  */
pixmapFromRepresentativeIndex(const QVariant & index,const QSize & size)448 QPixmap GPSMarkerTiler::pixmapFromRepresentativeIndex(const QVariant& index, const QSize& size)
449 {
450     QPair<TileIndex, int> indexForPixmap = index.value<QPair<TileIndex, int> >();
451 
452     QPixmap thumbnail;
453     ItemInfo info(indexForPixmap.second);
454     d->thumbnailMap.insert(info.id(), index);
455 
456     if (d->thumbnailLoadThread->find(info.thumbnailIdentifier(), thumbnail, qMax(size.width() + 2, size.height() + 2)))
457     {
458         // digikam returns thumbnails with a border around them,
459         // but geolocation interface expects them without a border
460 
461         return thumbnail.copy(1, 1, thumbnail.size().width() - 2, thumbnail.size().height() - 2);
462     }
463     else
464     {
465         return QPixmap();
466     }
467 }
468 
469 /**
470  * @brief This function compares two marker indices.
471  */
indicesEqual(const QVariant & a,const QVariant & b) const472 bool GPSMarkerTiler::indicesEqual(const QVariant& a, const QVariant& b) const
473 {
474     QPair<TileIndex, int> firstIndex  = a.value<QPair<TileIndex, int> >();
475     QPair<TileIndex, int> secondIndex = b.value<QPair<TileIndex, int> >();
476 
477     QList<int> aIndicesList           = firstIndex.first.toIntList();
478     QList<int> bIndicesList           = secondIndex.first.toIntList();
479 
480     if ((firstIndex.second == secondIndex.second) && (aIndicesList == bIndicesList))
481     {
482         return true;
483     }
484 
485     return false;
486 }
487 
getTileGroupState(const TileIndex & tileIndex)488 GeoGroupState GPSMarkerTiler::getTileGroupState(const TileIndex& tileIndex)
489 {
490     const bool haveGlobalSelection = (d->mapGlobalGroupState & (FilteredPositiveMask | RegionSelectedMask));
491 
492     if (!haveGlobalSelection)
493     {
494         return SelectedNone;
495     }
496 
497     /// @todo Store this state in the tiles!
498 
499     MyTile* const tile = static_cast<MyTile*>(getTile(tileIndex, true));
500     GroupStateComputer tileStateComputer;
501 
502     for (int i = 0 ; i < tile->imagesId.count() ; ++i)
503     {
504         const GeoGroupState imageState = getImageState(tile->imagesId.at(i));
505 
506         tileStateComputer.addState(imageState);
507     }
508 
509     return tileStateComputer.getState();
510 }
511 
512 /**
513  * @brief The marker data is returned from the database in batches. This function takes and unites the batches.
514  */
slotMapImagesJobData(const QList<ItemListerRecord> & records)515 void GPSMarkerTiler::slotMapImagesJobData(const QList<ItemListerRecord>& records)
516 {
517     if (records.isEmpty())
518     {
519         return;
520     }
521 
522     Private::InternalJobs* internalJob = nullptr;
523 
524     for (int i = 0 ; i < d->jobs.count() ; ++i)
525     {
526         if (sender() == d->jobs.at(i).jobThread)
527         {
528             /// @todo Is this really safe?
529 
530             internalJob = &d->jobs[i];
531             break;
532         }
533     }
534 
535     if (!internalJob)
536     {
537         return;
538     }
539 
540     foreach (const ItemListerRecord &record, records)
541     {
542         if (record.extraValues.count() < 2)
543         {
544             // skip info without coordinates
545 
546             continue;
547         }
548 
549         GPSItemInfo entry;
550 
551         entry.id           = record.imageID;
552         entry.rating       = record.rating;
553         entry.dateTime     = record.creationDate;
554         entry.coordinates.setLatLon(record.extraValues.first().toDouble(), record.extraValues.last().toDouble());
555 
556         internalJob->dataFromDatabase << entry;
557     }
558 }
559 
560 /**
561  * @brief Now, all the marker data has been retrieved from the database. Here, the markers are sorted into tiles.
562  */
slotMapImagesJobResult()563 void GPSMarkerTiler::slotMapImagesJobResult()
564 {
565     int foundIndex = -1;
566 
567     for (int i = 0 ; i < d->jobs.count() ; ++i)
568     {
569         if (sender() == d->jobs.at(i).jobThread)
570         {
571             foundIndex = i;
572             break;
573         }
574     }
575 
576     if (foundIndex < 0)
577     {
578         // this should not happen, but ok...
579 
580         return;
581     }
582 
583     if (d->jobs.at(foundIndex).jobThread->hasErrors())
584     {
585         const QString& err = d->jobs.at(foundIndex).jobThread->errorsList().first();
586 
587         qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to list images in selected area: "
588                                        << err;
589 
590         // Pop-up a message about the error.
591 
592         DNotificationWrapper(QString(), err,
593                              DigikamApp::instance(), DigikamApp::instance()->windowTitle());
594     }
595 
596     // get the results from the job:
597 
598     const QList<GPSItemInfo> returnedItemInfo = d->jobs.at(foundIndex).dataFromDatabase;
599 
600     /// @todo Currently, we ignore the wanted level and just add the images
601 /*
602     const int wantedLevel = d->jobs.at(foundIndex).level;
603 */
604     // remove the finished job
605 
606     d->jobs[foundIndex].jobThread->cancel();
607     d->jobs[foundIndex].jobThread = nullptr;
608     d->jobs.removeAt(foundIndex);
609 
610     if (returnedItemInfo.isEmpty())
611     {
612         return;
613     }
614 
615     // QElapsedTimer elapsedTimer;
616     // elapsedTimer.start();
617 
618     for (int i = 0 ; i < returnedItemInfo.count() ; ++i)
619     {
620         const GPSItemInfo currentItemInfo = returnedItemInfo.at(i);
621 
622         if (!currentItemInfo.coordinates.hasCoordinates())
623         {
624             continue;
625         }
626 
627         if (d->imagesHash.contains(currentItemInfo.id))
628         {
629             continue;
630         }
631 
632         d->imagesHash.insert(currentItemInfo.id, currentItemInfo);
633 
634         const TileIndex markerTileIndex = TileIndex::fromCoordinates(currentItemInfo.coordinates, TileIndex::MaxLevel);
635         addMarkerToTileAndChildren(currentItemInfo.id, markerTileIndex);
636     }
637 
638     // qCDebug(DIGIKAM_GENERAL_LOG) << "added" << returnedItemInfo.count()
639     //                              << "markers in" << elapsedTimer.nsecsElapsed() / 1e9 << "seconds";
640 
641     emit signalTilesOrSelectionChanged();
642 }
643 
644 /**
645  * @brief Because of a call to pixmapFromRepresentativeIndex, some thumbnails are not yet loaded at the time of requesting.
646  * When each thumbnail loads, this slot is called and emits a signal that announces the map that the thumbnail is available.
647  */
slotThumbnailLoaded(const LoadingDescription & loadingDescription,const QPixmap & thumbnail)648 void GPSMarkerTiler::slotThumbnailLoaded(const LoadingDescription& loadingDescription, const QPixmap& thumbnail)
649 {
650     QVariant index = d->thumbnailMap.value(loadingDescription.thumbnailIdentifier().id);
651 /*
652     QPair<TileIndex, int> indexForPixmap =
653     index.value<QPair<TileIndex, int> >();
654 */
655     emit signalThumbnailAvailableForIndex(index, thumbnail.copy(1, 1, thumbnail.size().width() - 2, thumbnail.size().height() - 2));
656 }
657 
658 /**
659  * @brief Sets the map active/inactive
660  * @param state New state of the map, true means active.
661  */
setActive(const bool state)662 void GPSMarkerTiler::setActive(const bool state)
663 {
664     d->activeState = state;
665 }
666 
tileNew()667 AbstractMarkerTiler::Tile* GPSMarkerTiler::tileNew()
668 {
669     return new MyTile();
670 }
671 
672 /**
673  * @brief Receives notifications from the database when images were changed and updates the tiler
674  */
slotImageChange(const ImageChangeset & changeset)675 void GPSMarkerTiler::slotImageChange(const ImageChangeset& changeset)
676 {
677     const DatabaseFields::Set changes = changeset.changes();
678 
679     if (!((changes & DatabaseFields::LatitudeNumber)  ||
680           (changes & DatabaseFields::LongitudeNumber) ||
681           (changes & DatabaseFields::Altitude)))
682     {
683         return;
684     }
685 
686     foreach (const qlonglong& id, changeset.ids())
687     {
688         const ItemInfo newItemInfo(id);
689 
690         if (!newItemInfo.hasCoordinates())
691         {
692             if (d->imagesHash.contains(id))
693             {
694                 // the image has no coordinates any more
695                 // remove it from the tiles and the image list
696 
697                 const GPSItemInfo oldInfo           = d->imagesHash.value(id);
698                 const GeoCoordinates oldCoordinates = oldInfo.coordinates;
699                 const TileIndex oldTileIndex        = TileIndex::fromCoordinates(oldCoordinates, TileIndex::MaxLevel);
700 
701                 removeMarkerFromTileAndChildren(id, oldTileIndex);
702 
703                 d->imagesHash.remove(id);
704             }
705 
706             continue;
707         }
708 
709         GeoCoordinates newCoordinates(newItemInfo.latitudeNumber(), newItemInfo.longitudeNumber());
710 
711         if (newItemInfo.hasAltitude())
712         {
713             newCoordinates.setAlt(newItemInfo.altitudeNumber());
714         }
715 
716         if (d->imagesHash.contains(id))
717         {
718             // the image id is known, therefore the image has already been sorted into tiles.
719             // We assume that the coordinates of the image have changed.
720 
721             const GPSItemInfo oldInfo           = d->imagesHash.value(id);
722             const GeoCoordinates oldCoordinates = oldInfo.coordinates;
723             const GPSItemInfo currentItemInfo   = GPSItemInfo::fromIdCoordinatesRatingDateTime(id,
724                                                                                                newCoordinates,
725                                                                                                newItemInfo.rating(),
726                                                                                                newItemInfo.dateTime());
727 
728             d->imagesHash.insert(id, currentItemInfo);
729 
730             const TileIndex oldTileIndex        = TileIndex::fromCoordinates(oldCoordinates, TileIndex::MaxLevel);
731             const TileIndex newTileIndex        = TileIndex::fromCoordinates(newCoordinates, TileIndex::MaxLevel);
732 
733             // remove from old position
734             removeMarkerFromTileAndChildren(id, oldTileIndex);
735             // add at new position
736             addMarkerToTileAndChildren(id, newTileIndex);
737 
738         }
739         else
740         {
741             // the image is new, add it to the existing tiles
742 
743             const GPSItemInfo currentItemInfo = GPSItemInfo::fromIdCoordinatesRatingDateTime(id,
744                                                                                              newCoordinates,
745                                                                                              newItemInfo.rating(),
746                                                                                              newItemInfo.dateTime());
747 
748             d->imagesHash.insert(id, currentItemInfo);
749 
750             const TileIndex newMarkerTileIndex = TileIndex::fromCoordinates(currentItemInfo.coordinates, TileIndex::MaxLevel);
751 
752             addMarkerToTileAndChildren(id, newMarkerTileIndex);
753         }
754     }
755 
756     emit signalTilesOrSelectionChanged();
757 }
758 
759 /**
760  * @brief Receives notifications from the album model about new items
761  */
slotNewModelData(const QList<ItemInfo> & infoList)762 void GPSMarkerTiler::slotNewModelData(const QList<ItemInfo>& infoList)
763 {
764     // We do not actually store the data from the model, we just want
765     // to know that something was changed.
766     /// @todo Also monitor removed, reset, etc. signals
767 
768     Q_UNUSED(infoList);
769 
770     emit signalTilesOrSelectionChanged();
771 }
772 
setRegionSelection(const GeoCoordinates::Pair & sel)773 void GPSMarkerTiler::setRegionSelection(const GeoCoordinates::Pair& sel)
774 {
775     d->currentRegionSelection = sel;
776 
777     if (sel.first.hasCoordinates())
778     {
779         d->mapGlobalGroupState |= RegionSelectedMask;
780     }
781     else
782     {
783         d->mapGlobalGroupState &= ~RegionSelectedMask;
784     }
785 
786     emit signalTilesOrSelectionChanged();
787 }
788 
removeCurrentRegionSelection()789 void GPSMarkerTiler::removeCurrentRegionSelection()
790 {
791     d->currentRegionSelection.first.clear();
792 
793     d->mapGlobalGroupState &= ~RegionSelectedMask;
794 
795     emit signalTilesOrSelectionChanged();
796 }
797 
onIndicesClicked(const ClickInfo & clickInfo)798 void GPSMarkerTiler::onIndicesClicked(const ClickInfo& clickInfo)
799 {
800     /// @todo Also handle the representative index
801 
802     QList<qlonglong> clickedImagesId;
803 
804     foreach (const TileIndex& tileIndex, clickInfo.tileIndicesList)
805     {
806         clickedImagesId << getTileMarkerIds(tileIndex);
807     }
808 
809     int repImageId = -1;
810 
811     if (clickInfo.representativeIndex.canConvert<QPair<TileIndex, int> >())
812     {
813         repImageId = clickInfo.representativeIndex.value<QPair<TileIndex, int> >().second;
814     }
815 
816     if (clickInfo.currentMouseMode == MouseModeSelectThumbnail && d->selectionModel)
817     {
818         /**
819          * @todo This does not work properly, because not all images in a tile
820          * may be selectable because some of them are outside of the region selection
821          */
822         const bool doSelect = (clickInfo.groupSelectionState & SelectedMask) != SelectedAll;
823 
824         const QItemSelectionModel::SelectionFlags selectionFlags =
825                     (doSelect ? QItemSelectionModel::Select : QItemSelectionModel::Deselect)
826                     | QItemSelectionModel::Rows;
827 
828         for (int i = 0 ; i < clickedImagesId.count() ; ++i)
829         {
830             const QModelIndex currentIndex = d->imageFilterModel->indexForImageId(clickedImagesId.at(i));
831 
832             if (d->selectionModel->isSelected(currentIndex) != doSelect)
833             {
834                 d->selectionModel->select(currentIndex, selectionFlags);
835             }
836         }
837 
838         if (repImageId >= 0)
839         {
840             const QModelIndex repImageIndex = d->imageFilterModel->indexForImageId(repImageId);
841 
842             if (repImageIndex.isValid())
843             {
844                 d->selectionModel->setCurrentIndex(repImageIndex, selectionFlags);
845             }
846         }
847     }
848     else if (clickInfo.currentMouseMode == MouseModeFilter)
849     {
850         setPositiveFilterIsActive(true);
851         emit signalModelFilteredImages(clickedImagesId);
852     }
853 }
854 
getTileMarkerIds(const TileIndex & tileIndex)855 QList<qlonglong> GPSMarkerTiler::getTileMarkerIds(const TileIndex& tileIndex)
856 {
857     Q_ASSERT(tileIndex.level() <= TileIndex::MaxLevel);
858 
859     const MyTile* const myTile = static_cast<MyTile*>(getTile(tileIndex, true));
860 
861     if (!myTile)
862     {
863         return QList<qlonglong>();
864     }
865 
866     return myTile->imagesId;
867 }
868 
getGlobalGroupState()869 GeoGroupState GPSMarkerTiler::getGlobalGroupState()
870 {
871     return d->mapGlobalGroupState;
872 }
873 
getImageState(const qlonglong imageId)874 GeoGroupState GPSMarkerTiler::getImageState(const qlonglong imageId)
875 {
876     GeoGroupState imageState;
877 
878     // is the image inside the region selection?
879 
880     if (d->mapGlobalGroupState & RegionSelectedMask)
881     {
882         const QModelIndex imageAlbumModelIndex = d->imageAlbumModel->indexForImageId(imageId);
883 
884         if (imageAlbumModelIndex.isValid())
885         {
886             imageState |= RegionSelectedAll;
887         }
888         else
889         {
890             // not inside region selection, therefore
891             // no other flags can apply
892 
893             return RegionSelectedNone;
894         }
895     }
896 
897     // is the image positively filtered?
898 
899     if (d->mapGlobalGroupState & FilteredPositiveMask)
900     {
901         const QModelIndex imageIndexInFilterModel = d->imageFilterModel->indexForImageId(imageId);
902 
903         if (imageIndexInFilterModel.isValid())
904         {
905             imageState |= FilteredPositiveAll;
906 
907             // is the image selected?
908 
909             if (d->selectionModel->hasSelection())
910             {
911                 if (d->selectionModel->isSelected(imageIndexInFilterModel))
912                 {
913                     imageState |= SelectedAll;
914                 }
915             }
916         }
917         else
918         {
919             // the image is not positively filtered, therefore it can
920             // not be selected
921 
922             return imageState;
923         }
924     }
925     else
926     {
927         // is the image selected?
928 
929         if (d->selectionModel->hasSelection())
930         {
931             const QModelIndex imageIndexInFilterModel = d->imageFilterModel->indexForImageId(imageId);
932 
933             if (d->selectionModel->isSelected(imageIndexInFilterModel))
934             {
935                 imageState |= SelectedAll;
936             }
937         }
938     }
939 
940     return imageState;
941 }
942 
setPositiveFilterIsActive(const bool state)943 void GPSMarkerTiler::setPositiveFilterIsActive(const bool state)
944 {
945     if (state)
946     {
947         d->mapGlobalGroupState |= FilteredPositiveMask;
948     }
949     else
950     {
951         d->mapGlobalGroupState &= ~FilteredPositiveMask;
952     }
953 
954     /// @todo Somehow, a delay is necessary before emitting this signal - probably the order in which the filtering
955     /// is propagated to other parts of digikam is wrong or just takes too long
956 
957     QTimer::singleShot(100, this, SIGNAL(signalTilesOrSelectionChanged()));
958 /*
959     emit signalTilesOrSelectionChanged();
960 */
961 }
962 
slotSelectionChanged(const QItemSelection & selected,const QItemSelection & deselected)963 void GPSMarkerTiler::slotSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected)
964 {
965     /// @todo Buffer this information, update the tiles, etc.
966 
967     Q_UNUSED(selected);
968     Q_UNUSED(deselected);
969 
970     emit signalTilesOrSelectionChanged();
971 }
972 
removeMarkerFromTileAndChildren(const qlonglong imageId,const TileIndex & markerTileIndex)973 void GPSMarkerTiler::removeMarkerFromTileAndChildren(const qlonglong imageId, const TileIndex& markerTileIndex)
974 {
975     MyTile* currentParentTile = nullptr;
976     MyTile* currentTile       = static_cast<MyTile*>(rootTile());
977 
978     for (int level = 0 ; level <= markerTileIndex.level() ; ++level)
979     {
980         currentTile->imagesId.removeOne(imageId);
981 
982         if (currentTile->imagesId.isEmpty())
983         {
984             if (currentTile == rootTile())
985             {
986                 break;
987             }
988 
989             // this tile can be deleted
990 
991             if (currentParentTile)
992             {
993                 currentParentTile->deleteChild(currentTile);
994             }
995 
996             break;
997         }
998 
999         currentParentTile = currentTile;
1000         currentTile       = static_cast<MyTile*>(currentParentTile->getChild(markerTileIndex.at(level)));
1001 
1002         if (!currentTile)
1003         {
1004             break;
1005         }
1006     }
1007 }
1008 
addMarkerToTileAndChildren(const qlonglong imageId,const TileIndex & markerTileIndex)1009 void GPSMarkerTiler::addMarkerToTileAndChildren(const qlonglong imageId, const TileIndex& markerTileIndex)
1010 {
1011     MyTile* currentTile = static_cast<MyTile*>(rootTile());
1012 
1013     for (int level = 0 ; level <= markerTileIndex.level() ; ++level)
1014     {
1015         currentTile->imagesId.append(imageId);
1016 
1017         if (currentTile->childrenEmpty())
1018         {
1019             break;
1020         }
1021 
1022         MyTile* nextTile = static_cast<MyTile*>(currentTile->getChild(markerTileIndex.at(level)));
1023 
1024         if (!nextTile)
1025         {
1026             nextTile = static_cast<MyTile*>(tileNew());
1027             currentTile->addChild(markerTileIndex.at(level), nextTile);
1028         }
1029 
1030         currentTile = nextTile;
1031     }
1032 }
1033 
1034 } // namespace Digikam
1035