diff --git a/Map/MapView.cpp b/Map/MapView.cpp index 2cfe97cc..23f42798 100644 --- a/Map/MapView.cpp +++ b/Map/MapView.cpp @@ -1,470 +1,606 @@ /* Copyright (C) 2014-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ // Local includes #include "MapView.h" #include "Logging.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { const QString MAPVIEW_FLOATER_VISIBLE_CONFIG_PREFIX = QStringLiteral("MarbleFloaterVisible "); const QStringList MAPVIEW_RENDER_POSITION({ QStringLiteral("HOVERS_ABOVE_SURFACE") }); const QVector WANTED_FLOATERS { QStringLiteral("Compass"), QStringLiteral("Scale Bar"), QStringLiteral("Navigation"), QStringLiteral("Overview Map") }; // when the angular resolution is smaller than fineResolution, all details should be shown -constexpr qreal fineResolution = 0.000001; +constexpr qreal FINE_RESOLUTION = 0.000001; // size of the markers in screen coordinates (pixel) -constexpr int markerSizePx = 40; +constexpr int MARKER_SIZE_PX = 40; +// levels of clustering for geo coordinates +constexpr int MAP_CLUSTER_LEVELS = 10; +static_assert(MAP_CLUSTER_LEVELS > 0, "At least one level of clustering is needed for the map."); +static_assert(MAP_CLUSTER_LEVELS < 32, "See coarsenBinAddress to know why this is a bad idea."); /** * @brief computeBinAddress calculates a "bin" for grouping coordinates that are near each other. * Using a signed 32-bit integer value allows for 2 decimal places of either coordinate part, * which is roughly equivalent to a spatial resolution of 1 km. * @param coords (more or less) precise coordinates * @return imprecise coordinates */ Map::GeoBinAddress computeBinAddress(const Map::GeoCoordinates &coords) { qint64 lat = qRound(coords.lat() * 100); qint64 lon = qRound(coords.lon() * 100); return static_cast((lat << 32) + lon); } +/** + * @brief coarsenBinAddress takes A GeoBinAddress and reduces its precision. + * @param addr the address to be reduced in accuracy + * @param level how many binary digits should be nulled out + * @return + */ +Map::GeoBinAddress coarsenBinAddress(Map::GeoBinAddress addr, int level) +{ + // zero out the rightmost bits + quint64 mask = 0xffffffffffffffff << level; + // duplicate the mask onto the higher 32 bits + mask = ((mask << 32) & mask); + // apply mask + return addr & mask; +} + +/** + * @brief buildClusterMap fills the lodMap by putting the GeoBin in GeoClusters + * of decreasing levels of detail. + * @param lodMap a vector containing a map for each level of detail + * @param binAddress the GeoBinAddress for the newly added GeoBin + * @param bin the GeoBin to add to the lodMap + */ +void buildClusterMap(QVector> &lodMap, + Map::GeoBinAddress binAddress, const Map::GeoBin *bin) +{ + const Map::GeoCluster *cluster = bin; + for (int lvl = 1; lvl <= MAP_CLUSTER_LEVELS; lvl++) { + QHash &map = lodMap[lvl - 1]; + binAddress = coarsenBinAddress(binAddress, lvl); + qCDebug(MapLog) << "adding GeoCluster with address" << binAddress << "at level" << lvl; + if (map.contains(binAddress)) { + map[binAddress]->addSubCluster(cluster); + break; + } else { + map.insert(binAddress, new Map::GeoCluster(lvl)); + map[binAddress]->addSubCluster(cluster); + cluster = map[binAddress]; + } + } +} + /** * @brief extendGeoDataLatLonBox extend the given GeoDataLatLonBox to encompass the given coordinates. * @param box * @param coords */ void extendGeoDataLatLonBox(Marble::GeoDataLatLonBox &box, const Map::GeoCoordinates &coords) { if (box.isEmpty()) { box.setEast(coords.lon(), Marble::GeoDataCoordinates::Degree); box.setWest(coords.lon(), Marble::GeoDataCoordinates::Degree); box.setNorth(coords.lat(), Marble::GeoDataCoordinates::Degree); box.setSouth(coords.lat(), Marble::GeoDataCoordinates::Degree); } else { if (box.east(Marble::GeoDataCoordinates::Degree) < coords.lon()) { box.setEast(coords.lon(), Marble::GeoDataCoordinates::Degree); } if (box.west(Marble::GeoDataCoordinates::Degree) > coords.lon()) { box.setWest(coords.lon(), Marble::GeoDataCoordinates::Degree); } if (box.north(Marble::GeoDataCoordinates::Degree) < coords.lat()) { box.setNorth(coords.lat(), Marble::GeoDataCoordinates::Degree); } if (box.south(Marble::GeoDataCoordinates::Degree) > coords.lat()) { box.setSouth(coords.lat(), Marble::GeoDataCoordinates::Degree); } } } } +Marble::GeoDataLatLonAltBox Map::GeoCluster::boundingRegion() const +{ + if (m_boundingRegion.isEmpty()) { + for (const auto &subCluster : m_subClusters) { + m_boundingRegion |= subCluster->boundingRegion(); + } + } + return m_boundingRegion; +} + +Marble::GeoDataCoordinates Map::GeoCluster::center() const +{ + // TODO(jzarl): check how this compares to e.g. the center of all coordinates instead: + return boundingRegion().center(); +} + +void Map::GeoCluster::render(Marble::GeoPainter *painter, const Marble::ViewportParams &viewPortParams, const QPixmap &alternatePixmap, Map::MapStyle style) const +{ + const auto viewPort = viewPortParams.viewLatLonAltBox(); + + if (!viewPort.contains(boundingRegion())) + return; + + if (viewPortParams.resolves(boundingRegion(), MARKER_SIZE_PX) + || (boundingRegion().isNull() && viewPortParams.angularResolution() < m_resolution)) { + // if the region takes up enough screen space, we should display the subclusters individually. + // if all images have the same coordinates (null bounding region), this will never happen + // -> in this case, show the images when we're zoomed in enough + for (const auto &subCluster : m_subClusters) { + subCluster->render(painter, viewPortParams, alternatePixmap, style); + } + } else { + qCDebug(MapLog) << "GeoCluster at" << center().toString() << "has" << size() << "images."; + painter->setOpacity(0.5); + if (viewPortParams.angularResolution() < m_resolution) { + qCDebug(MapLog) << "GeoCluster: drawing area"; + // high resolution -> draw in geo coordinates to represent the area of the images + // the size was empirically determined + qreal sizeDeg = 1.5 * (boundingRegion().width(Marble::GeoDataCoordinates::Degree) + boundingRegion().height(Marble::GeoDataCoordinates::Degree)); + // sometimes, the boundingRegion is much smaller than the 40px circle + sizeDeg = qMax(sizeDeg, m_resolution); + // true -> size is in degree, not screen coordinates + painter->drawEllipse(center(), sizeDeg, sizeDeg, true); + } else { + qCDebug(MapLog) << "GeoCluster: drawing marker"; + // low resolution -> draw in screen coordinates to keep the region visible + painter->drawEllipse(center(), MARKER_SIZE_PX, MARKER_SIZE_PX); + } + painter->setOpacity(1); + QPen pen = painter->pen(); + painter->setPen(QPen(Qt::black)); + painter->drawText(center(), i18nc("The number of images in an area of the map", "%1", size()), -0.5 * MARKER_SIZE_PX, 0.5 * MARKER_SIZE_PX, MARKER_SIZE_PX, MARKER_SIZE_PX, QTextOption(Qt::AlignCenter)); + painter->setPen(pen); + } +} + +int Map::GeoCluster::size() const +{ + if (m_size == 0) { + for (const auto &subCluster : m_subClusters) { + m_size += subCluster->size(); + } + } + return m_size; +} + +Map::GeoCluster::GeoCluster(int lvl) + : m_resolution(FINE_RESOLUTION * (2 << lvl)) +{ +} + +void Map::GeoCluster::addSubCluster(const Map::GeoCluster *subCluster) +{ + m_subClusters.append(subCluster); +} + +Map::GeoBin::GeoBin() + : GeoCluster(0) +{ +} + void Map::GeoBin::addImage(DB::ImageInfoPtr image) { m_images.append(image); extendGeoDataLatLonBox(m_boundingRegion, image->coordinates()); } -Marble::GeoDataLatLonBox Map::GeoBin::boundingRegion() const +Marble::GeoDataLatLonAltBox Map::GeoBin::boundingRegion() const { return m_boundingRegion; } -Marble::GeoDataCoordinates Map::GeoBin::center() const -{ - // TODO(jzarl): check how this compares to e.g. the center of all coordinates instead: - return m_boundingRegion.center(); -} - void Map::GeoBin::render(Marble::GeoPainter *painter, const Marble::ViewportParams &viewPortParams, const QPixmap &alternatePixmap, MapStyle style) const { const auto viewPort = viewPortParams.viewLatLonAltBox(); - if (viewPortParams.resolves(boundingRegion(), markerSizePx) - || (boundingRegion().isNull() && viewPortParams.angularResolution() < fineResolution)) { + qCDebug(MapLog) << "GeoBin at" << center().toString() << "has" << size() << "images."; + if (viewPortParams.resolves(boundingRegion(), MARKER_SIZE_PX) + || (boundingRegion().isNull() && viewPortParams.angularResolution() < FINE_RESOLUTION)) { + qCDebug(MapLog) << "GeoBin: drawing individual images"; // if the region takes up enough screen space, we should display the images. // if all images have the same coordinates (null bounding region), this will never happen // -> in this case, show the images when we're zoomed in enough for (const DB::ImageInfoPtr &image : m_images) { const Marble::GeoDataCoordinates pos(image->coordinates().lon(), image->coordinates().lat(), image->coordinates().alt(), Marble::GeoDataCoordinates::Degree); if (viewPort.contains(pos)) { if (style == MapStyle::ShowPins) { painter->drawPixmap(pos, alternatePixmap); } else { // FIXME(l3u) Maybe we should cache the scaled thumbnails? - painter->drawPixmap(pos, ImageManager::ThumbnailCache::instance()->lookup(image->fileName()).scaled(QSize(markerSizePx, markerSizePx), Qt::KeepAspectRatio)); + painter->drawPixmap(pos, ImageManager::ThumbnailCache::instance()->lookup(image->fileName()).scaled(QSize(MARKER_SIZE_PX, MARKER_SIZE_PX), Qt::KeepAspectRatio)); } } } } else { painter->setOpacity(0.5); - if (viewPortParams.angularResolution() < fineResolution) { + if (viewPortParams.angularResolution() < FINE_RESOLUTION) { + qCDebug(MapLog) << "GeoBin: drawing area"; // high resolution -> draw in geo coordinates to represent the area of the images // the size was empirically determined qreal sizeDeg = 1.5 * (boundingRegion().width(Marble::GeoDataCoordinates::Degree) + boundingRegion().height(Marble::GeoDataCoordinates::Degree)); // sometimes, the boundingRegion is much smaller than the 40px circle - sizeDeg = qMax(sizeDeg, fineResolution); + sizeDeg = qMax(sizeDeg, FINE_RESOLUTION); // true -> size is in degree, not screen coordinates painter->drawEllipse(center(), sizeDeg, sizeDeg, true); } else { + qCDebug(MapLog) << "GeoBin: drawing marker"; // low resolution -> draw in screen coordinates to keep the region visible - painter->drawEllipse(center(), markerSizePx, markerSizePx); + painter->drawEllipse(center(), MARKER_SIZE_PX, MARKER_SIZE_PX); } painter->setOpacity(1); QPen pen = painter->pen(); painter->setPen(QPen(Qt::black)); - painter->drawText(center(), i18nc("The number of images in an area of the map", "%1", size()), -0.5 * markerSizePx, 0.5 * markerSizePx, markerSizePx, markerSizePx, QTextOption(Qt::AlignCenter)); + painter->drawText(center(), i18nc("The number of images in an area of the map", "%1", size()), -0.5 * MARKER_SIZE_PX, 0.5 * MARKER_SIZE_PX, MARKER_SIZE_PX, MARKER_SIZE_PX, QTextOption(Qt::AlignCenter)); painter->setPen(pen); } } int Map::GeoBin::size() const { return m_images.size(); } Map::MapView::MapView(QWidget *parent, UsageType type) : QWidget(parent) { if (type == UsageType::MapViewWindow) { setWindowFlags(Qt::Window); setAttribute(Qt::WA_DeleteOnClose); } QVBoxLayout *layout = new QVBoxLayout(this); m_statusLabel = new QLabel; m_statusLabel->setAlignment(Qt::AlignCenter); m_statusLabel->hide(); layout->addWidget(m_statusLabel); m_mapWidget = new Marble::MarbleWidget; m_mapWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_mapWidget->setProjection(Marble::Mercator); m_mapWidget->setMapThemeId(QStringLiteral("earth/openstreetmap/openstreetmap.dgml")); #ifdef MARBLE_HAS_regionSelected_NEW connect(m_mapWidget, &Marble::MarbleWidget::regionSelected, this, &Map::MapView::updateRegionSelection); #else connect(m_mapWidget, &Marble::MarbleWidget::regionSelected, this, &Map::MapView::updateRegionSelectionOld); #endif m_mapWidget->addLayer(this); layout->addWidget(m_mapWidget); m_mapWidget->show(); QHBoxLayout *controlLayout = new QHBoxLayout; layout->addLayout(controlLayout); // KPA's control buttons m_kpaButtons = new QWidget; QHBoxLayout *kpaButtonsLayout = new QHBoxLayout(m_kpaButtons); controlLayout->addWidget(m_kpaButtons); QPushButton *saveButton = new QPushButton; saveButton->setFlat(true); saveButton->setIcon(QPixmap(SmallIcon(QStringLiteral("media-floppy")))); saveButton->setToolTip(i18n("Save the current map settings")); kpaButtonsLayout->addWidget(saveButton); connect(saveButton, &QPushButton::clicked, this, &MapView::saveSettings); m_setLastCenterButton = new QPushButton; m_setLastCenterButton->setFlat(true); m_setLastCenterButton->setIcon(QPixmap(SmallIcon(QStringLiteral("go-first")))); m_setLastCenterButton->setToolTip(i18n("Go to last map position")); kpaButtonsLayout->addWidget(m_setLastCenterButton); connect(m_setLastCenterButton, &QPushButton::clicked, this, &MapView::setLastCenter); QPushButton *showThumbnails = new QPushButton; showThumbnails->setFlat(true); showThumbnails->setIcon(QPixmap(SmallIcon(QStringLiteral("view-preview")))); showThumbnails->setToolTip(i18n("Show thumbnails")); kpaButtonsLayout->addWidget(showThumbnails); showThumbnails->setCheckable(true); showThumbnails->setChecked(m_showThumbnails); connect(showThumbnails, &QPushButton::clicked, this, &MapView::setShowThumbnails); // Marble floater control buttons m_floaters = new QWidget; QHBoxLayout *floatersLayout = new QHBoxLayout(m_floaters); controlLayout->addStretch(); controlLayout->addWidget(m_floaters); KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup group = config->group(QStringLiteral("MapView")); for (const Marble::RenderPlugin *plugin : m_mapWidget->renderPlugins()) { if (plugin->renderType() != Marble::RenderPlugin::PanelRenderType) { continue; } const QString name = plugin->name(); if (!WANTED_FLOATERS.contains(name)) { continue; } QPushButton *button = new QPushButton; button->setCheckable(true); button->setFlat(true); button->setChecked(plugin->action()->isChecked()); button->setToolTip(plugin->description()); button->setProperty("floater", name); QPixmap icon = plugin->action()->icon().pixmap(QSize(20, 20)); if (icon.isNull()) { icon = QPixmap(20, 20); icon.fill(Qt::white); } button->setIcon(icon); connect(plugin->action(), &QAction::toggled, button, &QPushButton::setChecked); connect(button, &QPushButton::toggled, plugin->action(), &QAction::setChecked); floatersLayout->addWidget(button); const QVariant checked = group.readEntry(MAPVIEW_FLOATER_VISIBLE_CONFIG_PREFIX + name, true); button->setChecked(checked.toBool()); } m_pin = QPixmap(QStandardPaths::locate(QStandardPaths::DataLocation, QStringLiteral("pics/pin.png"))); } void Map::MapView::clear() { - m_geoBins.clear(); m_markersBox.clear(); m_regionSelected = false; } void Map::MapView::addImage(DB::ImageInfoPtr image) { - const GeoBinAddress binAddress = computeBinAddress(image->coordinates()); - m_geoBins[binAddress].addImage(image); - - // Update the viewport for zoomToMarkers() - extendGeoDataLatLonBox(m_markersBox, image->coordinates()); } void Map::MapView::addImages(const DB::ImageSearchInfo &searchInfo) { QElapsedTimer timer; timer.start(); displayStatus(MapStatus::Loading); DB::FileNameList images = DB::ImageDB::instance()->search(searchInfo); int count = 0; int total = 0; + QHash baseBins; + // put images in bins for (const auto &imageInfo : images) { DB::ImageInfoPtr image = imageInfo.info(); total++; if (image->coordinates().hasCoordinates()) { count++; - addImage(image); + const GeoBinAddress binAddress = computeBinAddress(image->coordinates()); + if (!baseBins.contains(binAddress)) { + baseBins.insert(binAddress, new GeoBin()); + } + baseBins[binAddress]->addImage(image); + // Update the viewport for zoomToMarkers() + extendGeoDataLatLonBox(m_markersBox, image->coordinates()); } } displayStatus(MapStatus::SearchCoordinates); - qCDebug(TimingLog) << "MapView::addImages(): added" << count << "of" << total << "images in" - << timer.elapsed() << "ms."; + qCInfo(TimingLog) << "MapView::addImages(): added" << count << "of" << total << "images in" + << timer.elapsed() << "ms."; + + timer.restart(); + QVector> clusters { MAP_CLUSTER_LEVELS }; + // aggregate bins to clusters + for (auto it = baseBins.constBegin(); it != baseBins.constEnd(); ++it) { + buildClusterMap(clusters, it.key(), it.value()); + count++; + } + Q_ASSERT(clusters[MAP_CLUSTER_LEVELS - 1].size() > 0); + for (int lvl = 0; lvl < MAP_CLUSTER_LEVELS; lvl++) { + qCInfo(MapLog) << "MapView:" << clusters[lvl].size() << "clusters on level" << lvl; + } + m_geoClusters = clusters[MAP_CLUSTER_LEVELS - 1]; + qCDebug(TimingLog) << "MapView::addImages(): aggregated" << count << "GeoClusters in" << timer.elapsed() << "ms."; } void Map::MapView::zoomToMarkers() { m_mapWidget->centerOn(m_markersBox); } void Map::MapView::setCenter(const DB::ImageInfoPtr image) { m_lastCenter = image->coordinates(); setLastCenter(); } void Map::MapView::saveSettings() { KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup group = config->group(QStringLiteral("MapView")); for (const QPushButton *button : m_floaters->findChildren()) { group.writeEntry(MAPVIEW_FLOATER_VISIBLE_CONFIG_PREFIX + button->property("floater").toString(), button->isChecked()); } config->sync(); QMessageBox::information(this, i18n("Map view"), i18n("Settings saved!")); } void Map::MapView::setShowThumbnails(bool state) { m_showThumbnails = state; m_mapWidget->reloadMap(); } void Map::MapView::displayStatus(MapStatus status) { switch (status) { case MapStatus::Loading: m_statusLabel->setText(i18n("Loading coordinates from the images ...")); m_statusLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_statusLabel->show(); m_mapWidget->hide(); m_regionSelected = false; m_setLastCenterButton->setEnabled(false); break; case MapStatus::ImageHasCoordinates: m_statusLabel->hide(); m_regionSelected = false; m_statusLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); m_mapWidget->show(); m_setLastCenterButton->show(); m_setLastCenterButton->setEnabled(true); break; case MapStatus::ImageHasNoCoordinates: m_statusLabel->setText(i18n("This image does not contain geographic coordinates.")); m_statusLabel->show(); m_statusLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_mapWidget->hide(); m_setLastCenterButton->show(); m_setLastCenterButton->setEnabled(false); break; case MapStatus::SomeImagesHaveNoCoordinates: m_statusLabel->setText(i18n("Some of the selected images do not contain geographic " "coordinates.")); m_statusLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); m_statusLabel->show(); m_regionSelected = false; m_mapWidget->show(); m_setLastCenterButton->show(); m_setLastCenterButton->setEnabled(true); break; case MapStatus::SearchCoordinates: m_statusLabel->setText(i18n("Search for geographic coordinates.")); m_statusLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); m_statusLabel->show(); m_mapWidget->show(); m_mapWidget->centerOn(0.0, 0.0); m_setLastCenterButton->hide(); break; case MapStatus::NoImagesHaveNoCoordinates: m_statusLabel->setText(i18n("None of the selected images contain geographic " "coordinates.")); m_statusLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_statusLabel->show(); m_mapWidget->hide(); m_setLastCenterButton->show(); m_setLastCenterButton->setEnabled(false); break; } emit displayStatusChanged(status); } void Map::MapView::setLastCenter() { m_mapWidget->centerOn(m_lastCenter.lon(), m_lastCenter.lat()); } void Map::MapView::updateRegionSelection(const Marble::GeoDataLatLonBox &selection) { m_regionSelected = true; m_regionSelection = selection; emit newRegionSelected(getRegionSelection()); } #ifndef MARBLE_HAS_regionSelected_NEW void Map::MapView::updateRegionSelectionOld(const QList &selection) { Q_ASSERT(selection.length() == 4); // see also: https://cgit.kde.org/marble.git/commit/?id=ec1f7f554e9f6ca248b4a3b01dbf08507870687e Marble::GeoDataLatLonBox sel { selection.at(1), selection.at(3), selection.at(2), selection.at(0), Marble::GeoDataCoordinates::Degree }; updateRegionSelection(sel); } #endif Map::GeoCoordinates::LatLonBox Map::MapView::getRegionSelection() const { return GeoCoordinates::LatLonBox( m_regionSelection.north(Marble::GeoDataCoordinates::Degree), m_regionSelection.south(Marble::GeoDataCoordinates::Degree), m_regionSelection.east(Marble::GeoDataCoordinates::Degree), m_regionSelection.west(Marble::GeoDataCoordinates::Degree)); } bool Map::MapView::regionSelected() const { return m_regionSelected; } QStringList Map::MapView::renderPosition() const { // we only ever paint on the same layer: return MAPVIEW_RENDER_POSITION; } bool Map::MapView::render(Marble::GeoPainter *painter, Marble::ViewportParams *viewPortParams, const QString &renderPos, Marble::GeoSceneLayer *) { Q_ASSERT(renderPos == renderPosition().first()); Q_ASSERT(viewPortParams != nullptr); QElapsedTimer timer; timer.start(); - int numDisplayed = 0; - int numBins = 0; painter->setBrush(QBrush(QColor(Qt::red).lighter())); painter->setPen(QColor(Qt::red)); - for (const auto &bin : m_geoBins) { - numBins++; - numDisplayed += bin.size(); - bin.render(painter, *viewPortParams, m_pin, m_showThumbnails ? MapStyle::ShowThumbnails : MapStyle::ShowPins); + for (const auto *bin : m_geoClusters) { + bin->render(painter, *viewPortParams, m_pin, m_showThumbnails ? MapStyle::ShowThumbnails : MapStyle::ShowPins); } - qCDebug(TimingLog) << "Map rendered" << numBins << "bins containing" << numDisplayed << "images in" - << timer.elapsed() << "ms."; + qCDebug(TimingLog) << "Map rendered in" << timer.elapsed() << "ms."; return true; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Map/MapView.h b/Map/MapView.h index 1e8c83b1..4fd20c8b 100644 --- a/Map/MapView.h +++ b/Map/MapView.h @@ -1,208 +1,243 @@ /* Copyright (C) 2014-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef MAPVIEW_H #define MAPVIEW_H #include "config-kpa-marble.h" #include "GeoCoordinates.h" #include #include #include #include #include #include -#include +#include #include namespace DB { class ImageSearchInfo; } namespace Marble { class MarbleWidget; -class GeoDataLatLonAltBox; } class QLabel; class QPushButton; namespace Map { /** * UsageType: determines whether the widget is used as a standalone widget * or within another widget (e.g. the AnnotationDialog). * @see Viewer::ViewerWidget::UsageType */ enum class UsageType { InlineMapView, MapViewWindow }; /** * MapStatus: determines the visibility and text of the status label and the visibility of the * map, depending on the availability of coordinates of the image(s) that are displayed. */ enum class MapStatus { Loading, ImageHasCoordinates, ImageHasNoCoordinates, NoImagesHaveNoCoordinates, SomeImagesHaveNoCoordinates, SearchCoordinates }; enum class MapStyle { ShowPins, ShowThumbnails }; +class GeoCluster +{ +public: + explicit GeoCluster(int lvl); + virtual ~GeoCluster() = default; + + void addSubCluster(const GeoCluster *subCluster); + /** + * @brief boundingRegion computes the bounding region for the GeoCluster + * The result is only computed once at the first call to the method. + * @return a GeoDataLatLonBox containing all images in all sub-clusters. + */ + virtual Marble::GeoDataLatLonAltBox boundingRegion() const; + /** + * @brief center + * @return the approximate geographical center of the GeoCluster + */ + virtual Marble::GeoDataCoordinates center() const; + virtual void render(Marble::GeoPainter *painter, const Marble::ViewportParams &viewPortParams, const QPixmap &alternatePixmap, MapStyle style) const; + /** + * @brief size + * The result is only computed once at the first call to the method. + * @return the number of images in all sub-clusters + */ + virtual int size() const; + +private: + mutable int m_size = 0; + qreal m_resolution; + QList m_subClusters; + +protected: + mutable Marble::GeoDataLatLonAltBox m_boundingRegion; +}; + /** * @brief The GeoBin class holds a number of images that are grouped into the same bin. * I.e. they are in the direct vicinity of each other. */ -class GeoBin +class GeoBin : public GeoCluster { public: + GeoBin(); void addImage(DB::ImageInfoPtr image); - Marble::GeoDataLatLonBox boundingRegion() const; - Marble::GeoDataCoordinates center() const; - void render(Marble::GeoPainter *painter, const Marble::ViewportParams &viewPort, const QPixmap &alternatePixmap, MapStyle style) const; - int size() const; + Marble::GeoDataLatLonAltBox boundingRegion() const override; + void render(Marble::GeoPainter *painter, const Marble::ViewportParams &viewPortParams, const QPixmap &alternatePixmap, MapStyle style) const override; + int size() const override; private: QList m_images; - Marble::GeoDataLatLonBox m_boundingRegion; }; /** * \brief A conveniently 64bit word-sized type that holds an imprecise representation of a GeoCoordinate. + * 64 bit allow for enough precision to represent the lat/lon part of the GeoCoordinate of a GeoBin, + * while also having nice properties for being used as a key to a hashmap lookup. */ using GeoBinAddress = quint64; class MapView : public QWidget, public Marble::LayerInterface { Q_OBJECT public: explicit MapView(QWidget *parent = nullptr, UsageType type = UsageType::InlineMapView); ~MapView() override = default; /** * Removes all images from the map. */ void clear(); /** * Add an image to the map. */ void addImage(DB::ImageInfoPtr image); /** * @brief addImages adds images matching a search info to the map. * @param searchInfo */ void addImages(const DB::ImageSearchInfo &searchInfo); /** * Sets the map's zoom so that all images on the map are visible. * If no images have been added, the zoom is not altered. */ void zoomToMarkers(); /** * Sets the state of the "Show Thumbnails" button on the map's control widget. */ void setShowThumbnails(bool state); /** * This sets the status label text and it's visibility, as well as the visibilty of the map * itself to the state indicated by the given MapStatus. */ void displayStatus(MapStatus status); GeoCoordinates::LatLonBox getRegionSelection() const; bool regionSelected() const; // LayerInterface: /** * @brief renderPosition tells the LayerManager what layers we (currently) want to paint on. * Part of the LayerInterface; called by the LayerManager. * @return */ QStringList renderPosition() const override; /** * @brief Render all markers onto the marbleWidget. * Part of the LayerInterface; called by the LayerManager. * @param painter the painter used by the LayerManager - * @param viewport information about the region being in view + * @param viewPortParams information about the region being in view * @param renderPos the layer name * @param layer always \c nullptr * @return \c true (return value is discarded by LayerManager::renderLayers()) */ bool render(Marble::GeoPainter *painter, Marble::ViewportParams *viewPortParams, const QString &renderPos, Marble::GeoSceneLayer *) override; Q_SIGNALS: void newRegionSelected(Map::GeoCoordinates::LatLonBox coordinates); void displayStatusChanged(MapStatus); public slots: /** * Centers the map on the coordinates of the given image. */ void setCenter(const DB::ImageInfoPtr image); private slots: void saveSettings(); void setLastCenter(); void updateRegionSelection(const Marble::GeoDataLatLonBox &selection); #ifndef MARBLE_HAS_regionSelected_NEW // remove once we don't care about Marble v17.12.3 and older anymore void updateRegionSelectionOld(const QList &selection); #endif private: // Variables Marble::MarbleWidget *m_mapWidget; QLabel *m_statusLabel; QPushButton *m_setLastCenterButton; GeoCoordinates m_lastCenter; QWidget *m_kpaButtons; QWidget *m_floaters; - QMap m_geoBins; + QHash m_geoClusters; Marble::GeoDataLatLonBox m_markersBox; bool m_showThumbnails = true; QPixmap m_pin; Marble::GeoDataLatLonBox m_regionSelection; bool m_regionSelected = false; }; } #endif // MAPVIEW_H // vi:expandtab:tabstop=4 shiftwidth=4: