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 }