File indexing completed on 2024-04-28 15:39:07

0001 // SPDX-FileCopyrightText: 2020-2024 Tobias Leupold <tl at stonemx dot de>
0002 //
0003 // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 
0005 // Local includes
0006 #include "MapWidget.h"
0007 #include "SharedObjects.h"
0008 #include "TracksLayer.h"
0009 #include "ImagesLayer.h"
0010 #include "Settings.h"
0011 #include "KGeoTag.h"
0012 #include "ImagesModel.h"
0013 #include "MimeHelper.h"
0014 #include "GeoDataModel.h"
0015 
0016 // Marble includes
0017 #include <marble/GeoPainter.h>
0018 #include <marble/AbstractFloatItem.h>
0019 #include <marble/MarbleWidgetInputHandler.h>
0020 #include <marble/ViewportParams.h>
0021 #include <marble/GeoDataLatLonAltBox.h>
0022 
0023 // KDE includes
0024 #include <KLocalizedString>
0025 
0026 // Qt includes
0027 #include <QDebug>
0028 #include <QDragEnterEvent>
0029 #include <QMimeData>
0030 #include <QDropEvent>
0031 #include <QMenu>
0032 #include <QAction>
0033 #include <QUrl>
0034 
0035 // C++ includes
0036 #include <functional>
0037 #include <utility>
0038 
0039 static QString s_licenseFloaterId = QStringLiteral("license");
0040 static QVector<QString> s_unsupportedFloaters = {
0041     s_licenseFloaterId,
0042     QStringLiteral("elevationprofile"),
0043     QStringLiteral("GpsInfo"),
0044     QStringLiteral("progress"),
0045     QStringLiteral("routing"),
0046     QStringLiteral("speedometer")
0047 };
0048 
0049 MapWidget::MapWidget(SharedObjects *sharedObjects, QWidget *parent)
0050     : Marble::MarbleWidget(parent),
0051       m_settings(sharedObjects->settings()),
0052       m_geoDataModel(sharedObjects->geoDataModel()),
0053       m_imagesModel(sharedObjects->imagesModel())
0054 {
0055     connect(this, &Marble::MarbleWidget::visibleLatLonAltBoxChanged,
0056             this, [this]
0057             {
0058                 Q_EMIT mapMoved(currentCenter());
0059             });
0060 
0061     setAcceptDrops(true);
0062 
0063     setProjection(Marble::Mercator);
0064     setMapThemeId(QStringLiteral("earth/openstreetmap/openstreetmap.dgml"));
0065 
0066     auto *tracksLayer = new TracksLayer(this, m_geoDataModel, &m_trackPen);
0067     auto *imagesLayer = new ImagesLayer(this, m_imagesModel);
0068     addLayer(tracksLayer);
0069     addLayer(imagesLayer);
0070 
0071     m_trackPen.setCapStyle(Qt::RoundCap);
0072     m_trackPen.setJoinStyle(Qt::RoundJoin);
0073     updateSettings();
0074 
0075     // Build a context menu
0076 
0077     m_contextMenu = new QMenu(this);
0078     m_contextMenu->addSection(i18n("Floating items"));
0079 
0080     const auto floatItemsList = floatItems();
0081     const auto visibility = m_settings->floatersVisibility();
0082 
0083     // Add a "Toggle crosshairs" action
0084     auto *crossHairsAction = m_contextMenu->addAction(i18n("Crosshairs"));
0085     crossHairsAction->setCheckable(true);
0086     connect(crossHairsAction, &QAction::toggled, this, &Marble::MarbleWidget::setShowCrosshairs);
0087     crossHairsAction->setChecked(m_settings->showCrosshairs());
0088     setShowCrosshairs(m_settings->showCrosshairs());
0089 
0090     // Add actions for all supported floaters
0091 
0092     m_contextMenu->addSeparator();
0093 
0094     for (const auto &item : floatItemsList) {
0095         const auto id = item->nameId();
0096         if (s_unsupportedFloaters.contains(id)) {
0097             auto item = floatItem(id);
0098             if (item != nullptr) {
0099                 item->setVisible(false);
0100             }
0101             continue;
0102         }
0103 
0104         auto *action = m_contextMenu->addAction(item->name());
0105         action->setIcon(item->icon());
0106         action->setData(id);
0107         action->setCheckable(true);
0108         action->setChecked(visibility.value(id));
0109         action->setData(id);
0110 
0111         m_floatersActions.append(action);
0112 
0113         connect(action, &QAction::toggled,
0114                 this, std::bind(&MapWidget::changeFloaterVisiblity, this, action));
0115     }
0116 
0117     // The "License" floater is always shown on startup (by Marble itself)
0118     auto *licenseFloater = floatItem(s_licenseFloaterId);
0119     if (licenseFloater != nullptr) {
0120         m_contextMenu->addSeparator();
0121         auto *licenseAction = m_contextMenu->addAction(licenseFloater->name());
0122         licenseAction->setCheckable(true);
0123         licenseAction->setChecked(true);
0124         licenseAction->setData(s_licenseFloaterId);
0125         connect(licenseAction, &QAction::toggled,
0126                 this, std::bind(&MapWidget::changeFloaterVisiblity, this, licenseAction));
0127         m_floatersActions.append(licenseAction);
0128     }
0129 
0130     // Don't use the MarbleWidget context menu, but our own
0131     auto *handler = inputHandler();
0132     handler->setMouseButtonPopupEnabled(Qt::RightButton, false);
0133     connect(handler, &Marble::MarbleWidgetInputHandler::rmbRequest,
0134             this, &MapWidget::showContextMenu);
0135 }
0136 
0137 void MapWidget::showContextMenu(int x, int y)
0138 {
0139     for (auto *action : std::as_const(m_floatersActions)) {
0140         action->blockSignals(true);
0141         action->setChecked(floatItem(action->data().toString())->visible());
0142         action->blockSignals(false);
0143     }
0144 
0145     m_contextMenu->exec(mapToGlobal(QPoint(x, y)));
0146 }
0147 
0148 void MapWidget::changeFloaterVisiblity(QAction *action)
0149 {
0150     floatItem(action->data().toString())->setVisible(action->isChecked());
0151 }
0152 
0153 void MapWidget::updateSettings()
0154 {
0155     m_trackPen.setColor(m_settings->trackColor());
0156     m_trackPen.setWidth(m_settings->trackWidth());
0157     m_trackPen.setStyle(m_settings->trackStyle());
0158     reloadMap();
0159 }
0160 
0161 void MapWidget::saveSettings()
0162 {
0163     // Save the crosshairs visibility
0164     m_settings->saveShowCrosshairs(showCrosshairs());
0165 
0166     // Save the floaters visibility
0167 
0168     QHash<QString, bool> visibility;
0169 
0170     const auto floatItemsList = floatItems();
0171     for (const auto &item : floatItemsList) {
0172         const auto id = item->nameId();
0173         if (s_unsupportedFloaters.contains(id)) {
0174             continue;
0175         }
0176         visibility.insert(id, item->visible());
0177     }
0178 
0179     m_settings->saveFloatersVisibility(visibility);
0180 
0181     // Save the current center point
0182     m_settings->saveMapCenter(currentCenter());
0183 
0184     // Save the zoom level
0185     m_settings->saveZoom(zoom());
0186 }
0187 
0188 void MapWidget::restoreSettings()
0189 {
0190     // Restore the floaters visibility
0191     const auto floatersVisibility = m_settings->floatersVisibility();
0192     const auto floatItemsList = floatItems();
0193     for (const auto &item : floatItemsList) {
0194         const auto name = item->nameId();
0195         if (floatersVisibility.contains(name)) {
0196             item->setVisible(floatersVisibility.value(name));
0197         }
0198     }
0199 
0200     // Restore map's last center point
0201     const auto center = m_settings->mapCenter();
0202     centerOn(center.lon(), center.lat());
0203 
0204     // Restore the last zoom level
0205     setZoom(m_settings->zoom());
0206 }
0207 
0208 void MapWidget::dragEnterEvent(QDragEnterEvent *event)
0209 {
0210     const auto mimeData = event->mimeData();
0211     const auto source = mimeData->data(KGeoTag::SourceImagesListMimeType);
0212 
0213     if (! source.isEmpty()) {
0214         // The drag most probably comes from a KGeoTag images list.
0215         // Nevertheless, we check if all URLs are present in the images model ;-)
0216         const auto urls = mimeData->urls();
0217         for (const auto &url : urls) {
0218             if (! m_imagesModel->contains(url.toLocalFile())) {
0219                 return;
0220             }
0221         }
0222 
0223     } else {
0224         // Possibly a request to load a GPX file
0225         if (MimeHelper::getUsablePaths(KGeoTag::DroppedOnMap, mimeData).isEmpty()) {
0226             return;
0227         }
0228     }
0229 
0230     event->acceptProposedAction();
0231 }
0232 
0233 void MapWidget::dropEvent(QDropEvent *event)
0234 {
0235     const auto mimeData = event->mimeData();
0236     if (! mimeData->hasUrls()) {
0237         return;
0238     }
0239 
0240     const auto source = mimeData->data(KGeoTag::SourceImagesListMimeType);
0241     if (! source.isEmpty()) {
0242         // Images dragged from an images list
0243 
0244         const auto urls = mimeData->urls();
0245         QVector<QString> paths;
0246 
0247         // Be sure to really have all images
0248         for (const auto &url : urls) {
0249             const auto path = url.toLocalFile();
0250             if (m_imagesModel->contains(path)) {
0251                 paths.append(path);
0252             }
0253         }
0254 
0255         // This should not happen ...
0256         if (paths.isEmpty()) {
0257             return;
0258         }
0259 
0260         const auto dropPosition = event->pos();
0261 
0262         qreal lon;
0263         qreal lat;
0264         if (! geoCoordinates(dropPosition.x(), dropPosition.y(), lon, lat,
0265                              Marble::GeoDataCoordinates::Degree)) {
0266             return;
0267         }
0268 
0269         for (const auto &path : paths) {
0270             m_imagesModel->setCoordinates(path, Coordinates(lon, lat, 0.0, true),
0271                                           KGeoTag::ManuallySet);
0272         }
0273 
0274         reloadMap();
0275         Q_EMIT imagesDropped(paths);
0276 
0277     } else {
0278         // Request to load a GPX file
0279         const auto usablePaths = MimeHelper::getUsablePaths(KGeoTag::DroppedOnMap, mimeData);
0280         if (! usablePaths.isEmpty()) {
0281             Q_EMIT requestLoadGpx(usablePaths);
0282         }
0283     }
0284 
0285     event->acceptProposedAction();
0286 }
0287 
0288 void MapWidget::centerImage(const QModelIndex &index)
0289 {
0290     if (! index.isValid()) {
0291         return;
0292     }
0293 
0294     const auto coordinates = index.data(KGeoTag::CoordinatesRole).value<Coordinates>();
0295     if (coordinates.isSet()) {
0296         centerCoordinates(coordinates);
0297     }
0298 }
0299 
0300 void MapWidget::centerCoordinates(const Coordinates &coordinates)
0301 {
0302     centerOn(coordinates.lon(), coordinates.lat());
0303 }
0304 
0305 void MapWidget::zoomToTrack(const QModelIndex &index)
0306 {
0307     centerOn(m_geoDataModel->trackBox(index));
0308 }
0309 
0310 void MapWidget::zoomToTracks(const QVector<QString> &paths)
0311 {
0312     Marble::GeoDataLatLonAltBox box;
0313     for (const auto &path : paths) {
0314         if (box.isEmpty()) {
0315             box = m_geoDataModel->trackBox(path);
0316         } else {
0317             box |= m_geoDataModel->trackBox(path);
0318         }
0319     }
0320     centerOn(box);
0321 }
0322 
0323 Coordinates MapWidget::currentCenter() const
0324 {
0325     const auto center = focusPoint();
0326     return Coordinates(center.longitude(Marble::GeoDataCoordinates::Degree),
0327                        center.latitude(Marble::GeoDataCoordinates::Degree),
0328                        0.0,
0329                        true);
0330 }