File indexing completed on 2024-05-05 04:22:10

0001 // SPDX-FileCopyrightText: 2019-2021 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0002 //
0003 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 
0005 #include "GeoCluster.h"
0006 #include "GeoCoordinates.h"
0007 #include "Logging.h"
0008 
0009 #include <DB/ImageInfo.h>
0010 #include <kpathumbnails/ThumbnailCache.h>
0011 
0012 #include <KLocalizedString>
0013 #include <QPen>
0014 #include <QSize>
0015 #include <marble/GeoDataLatLonBox.h>
0016 #include <marble/GeoPainter.h>
0017 #include <marble/ViewportParams.h>
0018 
0019 namespace
0020 {
0021 // when the angular resolution is smaller than fineResolution, all details should be shown
0022 constexpr qreal FINE_RESOLUTION = 0.000001;
0023 // the scale factor of the bounding box compared to the bounding box as drawn on the map
0024 constexpr qreal BOUNDING_BOX_SCALEFACTOR = 1.2;
0025 
0026 /**
0027  * @brief screenSize computes the screen size of a geographical region in pixels.
0028  * If one of the bounding box edges is not visible, a null QizeF is retunred.
0029  * @param viewPortParams the parameters of the current view port
0030  * @param box the geographical region
0031  * @return the size in pixels, or a null size
0032  */
0033 QSizeF screenSize(const Marble::ViewportParams &viewPortParams, const Marble::GeoDataLatLonBox &box, bool debug = false)
0034 {
0035     qreal east;
0036     qreal north;
0037     qreal west;
0038     qreal south;
0039     // if a point is not visible on screen, screenCoordinates() returns false
0040     // the result is still usable, though
0041     bool onScreen;
0042     onScreen = viewPortParams.screenCoordinates(box.east(Marble::GeoDataCoordinates::Radian),
0043                                                 box.north(Marble::GeoDataCoordinates::Radian),
0044                                                 east, north);
0045     onScreen &= viewPortParams.screenCoordinates(box.west(Marble::GeoDataCoordinates::Radian),
0046                                                  box.south(Marble::GeoDataCoordinates::Radian),
0047                                                  west, south);
0048     if (debug) {
0049         qCDebug(MapLog) << "coordinates" << east << "-" << west << "," << north << "-" << south << "are" << (onScreen ? "on screen" : "not (fully) on screen");
0050     }
0051     return QSizeF { qAbs(east - west), qAbs(north - south) };
0052 }
0053 
0054 /**
0055  * @brief screenRegion translates a bounding region into screen coordinates for drawing it.
0056  * The region has the size of what is actually drawn, not the size of the bounding region itself.
0057  * @param viewPortParams
0058  * @param region
0059  * @param minSizePx
0060  * @return the region in screen coordinates
0061  */
0062 QRectF screenRegion(const Marble::ViewportParams &viewPortParams, const Marble::GeoDataLatLonBox region, int minSizePx)
0063 {
0064     const QSizeF areaSizePx = screenSize(viewPortParams, region);
0065     // drawing a larger area gets nicer results on average:
0066     const qreal heightPx = qMax(BOUNDING_BOX_SCALEFACTOR * areaSizePx.height(), (qreal)minSizePx);
0067     const qreal widthPx = qMax(BOUNDING_BOX_SCALEFACTOR * areaSizePx.width(), (qreal)minSizePx);
0068     qreal left;
0069     qreal top;
0070     viewPortParams.screenCoordinates(region.west(Marble::GeoDataCoordinates::Radian),
0071                                      region.north(Marble::GeoDataCoordinates::Radian),
0072                                      left, top);
0073     return QRectF(left, top, widthPx, heightPx);
0074 }
0075 } // namespace
0076 
0077 Marble::GeoDataLatLonAltBox Map::GeoCluster::boundingRegion() const
0078 {
0079     if (m_boundingRegion.isEmpty()) {
0080         for (const auto &subCluster : m_subClusters) {
0081             m_boundingRegion |= subCluster->boundingRegion();
0082         }
0083     }
0084     return m_boundingRegion;
0085 }
0086 
0087 Marble::GeoDataCoordinates Map::GeoCluster::center() const
0088 {
0089     return boundingRegion().center();
0090 }
0091 
0092 const Map::GeoCluster *Map::GeoCluster::regionForPoint(QPoint pos) const
0093 {
0094     // only check child items if the cluster was not drawn on the map:
0095     if (m_renderedRegion.isEmpty()) {
0096         for (const auto &subCluster : m_subClusters) {
0097             auto cluster = subCluster->regionForPoint(pos);
0098             if (cluster && !cluster->isEmpty())
0099                 return cluster;
0100         }
0101     } else if (m_renderedRegion.contains(pos)) {
0102         qCDebug(MapLog) << "GeoCluster containing" << size() << "images matches point.";
0103         return this;
0104     }
0105     return nullptr;
0106 }
0107 
0108 void Map::GeoCluster::render(Marble::GeoPainter *painter, const Marble::ViewportParams &viewPortParams, const ThumbnailParams &thumbs, Map::MapStyle style) const
0109 {
0110     if (style == MapStyle::ForceShowThumbnails || size() == 1
0111         || viewPortParams.resolves(boundingRegion(), thumbs.thumbnailSizePx)
0112         || (viewPortParams.angularResolution() < FINE_RESOLUTION)) {
0113         m_renderedRegion = {};
0114         // if the region takes up enough screen space, we should display the subclusters individually.
0115         // if all images have the same coordinates (null bounding region), this will never happen
0116         // -> in this case, show the images when we're zoomed in enough
0117         renderSubItems(painter, viewPortParams, thumbs, style);
0118     } else {
0119         qCDebug(MapLog) << "GeoCluster has" << size() << "images.";
0120         QPen pen = painter->pen();
0121         const auto opacity = painter->opacity();
0122         painter->setOpacity(0.5);
0123         const QRectF screenRect = screenRegion(viewPortParams, boundingRegion(), thumbs.thumbnailSizePx);
0124         painter->drawRect(center(), screenRect.width(), screenRect.height(), false);
0125         m_renderedRegion = painter->regionFromRect(center(), screenRect.width(), screenRect.height(), false);
0126 #if MARBLE_VERSION < QT_VERSION_CHECK(21, 04, 0)
0127         // adjust region to match up with drawn region (see Marble bug https://bugs.kde.org/show_bug.cgi?id=431466):
0128         m_renderedRegion.translate(static_cast<int>(-0.5 * screenRect.width()), static_cast<int>(-0.5 * screenRect.height()));
0129 #endif
0130 #ifdef MARBLE_DEBUG_GEOPAINTER
0131         // draw clickable region for visual inspection:
0132         painter->setPen(Qt::green);
0133         painter->drawRect(*m_renderedRegion.begin());
0134 #endif
0135         painter->setOpacity(opacity);
0136         painter->setPen(QPen(Qt::black));
0137         painter->drawText(center(), i18nc("The number of images in an area of the map", "%1", size()), -0.5 * thumbs.thumbnailSizePx, 0.5 * thumbs.thumbnailSizePx, thumbs.thumbnailSizePx, thumbs.thumbnailSizePx, QTextOption(Qt::AlignCenter));
0138         painter->setPen(pen);
0139     }
0140 }
0141 
0142 int Map::GeoCluster::size() const
0143 {
0144     if (m_size == 0) {
0145         for (const auto &subCluster : m_subClusters) {
0146             m_size += subCluster->size();
0147         }
0148     }
0149     return m_size;
0150 }
0151 
0152 bool Map::GeoCluster::isEmpty() const
0153 {
0154     return (size() == 0);
0155 }
0156 
0157 void Map::GeoCluster::renderSubItems(Marble::GeoPainter *painter, const Marble::ViewportParams &viewPortParams, const ThumbnailParams &thumbs, Map::MapStyle style) const
0158 {
0159     for (const auto &subCluster : m_subClusters) {
0160         subCluster->render(painter, viewPortParams, thumbs, style);
0161     }
0162 }
0163 
0164 Map::GeoCluster::GeoCluster(int lvl)
0165     : m_level(lvl)
0166 {
0167 }
0168 
0169 void Map::GeoCluster::addSubCluster(const Map::GeoCluster *subCluster)
0170 {
0171     m_subClusters.append(subCluster);
0172 }
0173 
0174 Map::GeoBin::GeoBin()
0175     : GeoCluster(0)
0176     , m_thumbnailSizePx(0)
0177 {
0178 }
0179 
0180 void Map::GeoBin::addImage(DB::ImageInfoPtr image)
0181 {
0182     m_images.append(image);
0183     extendGeoDataLatLonBox(m_boundingRegion, image->coordinates());
0184 }
0185 
0186 Marble::GeoDataLatLonAltBox Map::GeoBin::boundingRegion() const
0187 {
0188     return m_boundingRegion;
0189 }
0190 
0191 int Map::GeoBin::size() const
0192 {
0193     return m_images.size();
0194 }
0195 
0196 void Map::GeoBin::renderSubItems(Marble::GeoPainter *painter, const Marble::ViewportParams &viewPortParams, const ThumbnailParams &thumbs, Map::MapStyle style) const
0197 {
0198     const auto viewPort = viewPortParams.viewLatLonAltBox();
0199     qCDebug(MapLog) << "GeoBin: drawing" << m_images.count() << "individual images";
0200     for (const DB::ImageInfoPtr &image : m_images) {
0201         const Marble::GeoDataCoordinates pos(image->coordinates().lon(), image->coordinates().lat(),
0202                                              image->coordinates().alt(),
0203                                              Marble::GeoDataCoordinates::Degree);
0204         if (viewPort.contains(pos)) {
0205             if (style == MapStyle::ShowPins) {
0206                 painter->drawPixmap(pos, thumbs.alternatePixmap);
0207             } else {
0208                 if (thumbs.thumbnailSizePx != m_thumbnailSizePx) {
0209                     m_scaledThumbnailCache.clear();
0210                     m_thumbnailSizePx = thumbs.thumbnailSizePx;
0211                 }
0212                 if (!m_scaledThumbnailCache.contains(image)) {
0213                     QPixmap thumb = thumbs.cache->lookup(image->fileName()).scaled(QSize(thumbs.thumbnailSizePx, thumbs.thumbnailSizePx), Qt::KeepAspectRatio);
0214                     m_scaledThumbnailCache.insert(image, thumb);
0215                 }
0216                 painter->drawPixmap(pos, m_scaledThumbnailCache.value(image));
0217             }
0218         }
0219     }
0220 }
0221 
0222 void Map::extendGeoDataLatLonBox(Marble::GeoDataLatLonBox &box, const Map::GeoCoordinates &coords)
0223 {
0224     Marble::GeoDataLatLonBox addition { coords.lat(), coords.lat(), coords.lon(), coords.lon(), Marble::GeoDataCoordinates::Degree };
0225     // let GeoDataLatLonBox::united() take care of the edge cases
0226     box |= addition;
0227 }