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