Marble

RoutingWidget.cpp
1// SPDX-License-Identifier: LGPL-2.1-or-later
2//
3// SPDX-FileCopyrightText: 2010 Dennis Nienhüser <nienhueser@kde.org>
4//
5
6#include "RoutingWidget.h"
7
8#include "AlternativeRoutesModel.h"
9#include "CloudRoutesDialog.h"
10#include "CloudSyncManager.h"
11#include "GeoDataAnimatedUpdate.h"
12#include "GeoDataCreate.h"
13#include "GeoDataDelete.h"
14#include "GeoDataDocument.h"
15#include "GeoDataFlyTo.h"
16#include "GeoDataIconStyle.h"
17#include "GeoDataLineString.h"
18#include "GeoDataLookAt.h"
19#include "GeoDataPlacemark.h"
20#include "GeoDataPlaylist.h"
21#include "GeoDataStyle.h"
22#include "GeoDataTour.h"
23#include "GeoDataTreeModel.h"
24#include "GeoDataUpdate.h"
25#include "Maneuver.h"
26#include "MarbleModel.h"
27#include "MarblePlacemarkModel.h"
28#include "MarbleWidget.h"
29#include "MarbleWidgetInputHandler.h"
30#include "Planet.h"
31#include "PlaybackAnimatedUpdateItem.h"
32#include "Route.h"
33#include "RouteRequest.h"
34#include "RouteSyncManager.h"
35#include "RoutingInputWidget.h"
36#include "RoutingLayer.h"
37#include "RoutingModel.h"
38#include "RoutingProfileSettingsDialog.h"
39#include "RoutingProfilesModel.h"
40#include "TourPlayback.h"
41
42#include <QFileDialog>
43#include <QKeyEvent>
44#include <QMouseEvent>
45#include <QPainter>
46#include <QProgressDialog>
47#include <QTimer>
48#include <QToolBar>
49#include <QToolButton>
50
51#include "ui_RoutingWidget.h"
52
53namespace Marble
54{
55
56struct WaypointInfo {
57 int index;
58 double distance; // distance to route start
59 GeoDataCoordinates coordinates;
60 Maneuver maneuver;
61 QString info;
62
63 WaypointInfo(int index_, double distance_, const GeoDataCoordinates &coordinates_, Maneuver maneuver_, const QString &info_)
64 : index(index_)
65 , distance(distance_)
66 , coordinates(coordinates_)
67 , maneuver(maneuver_)
68 , info(info_)
69 {
70 // nothing to do
71 }
72};
73
74class RoutingWidgetPrivate
75{
76public:
77 Ui::RoutingWidget m_ui;
78 MarbleWidget *const m_widget;
79 RoutingManager *const m_routingManager;
80 RoutingLayer *const m_routingLayer;
81 RoutingInputWidget *m_activeInput;
82 QList<RoutingInputWidget *> m_inputWidgets;
83 RoutingInputWidget *m_inputRequest;
84 QAbstractItemModel *const m_routingModel;
85 RouteRequest *const m_routeRequest;
86 RouteSyncManager *m_routeSyncManager;
87 bool m_zoomRouteAfterDownload;
88 QTimer m_progressTimer;
89 QList<QIcon> m_progressAnimation;
90 GeoDataDocument *m_document;
91 GeoDataTour *m_tour;
92 TourPlayback *m_playback;
93 int m_currentFrame;
94 int m_iconSize;
95 int m_collapse_width;
96 bool m_playing;
97 QString m_planetId;
98
99 QToolBar *m_toolBar = nullptr;
100
101 QToolButton *m_openRouteButton = nullptr;
102 QToolButton *m_saveRouteButton = nullptr;
103 QAction *m_cloudSyncSeparator = nullptr;
104 QAction *m_uploadToCloudAction = nullptr;
105 QAction *m_openCloudRoutesAction = nullptr;
106 QToolButton *m_addViaButton = nullptr;
107 QToolButton *m_reverseRouteButton = nullptr;
108 QToolButton *m_clearRouteButton = nullptr;
109 QToolButton *m_configureButton = nullptr;
110 QToolButton *m_playButton = nullptr;
111
112 QProgressDialog *m_routeUploadDialog = nullptr;
113
114 /** Constructor */
115 RoutingWidgetPrivate(RoutingWidget *parent, MarbleWidget *marbleWidget);
116
117 /**
118 * @brief Toggle between simple search view and route view
119 * If only one input field exists, hide all buttons
120 */
121 void adjustInputWidgets();
122
123 void adjustSearchButton();
124
125 /**
126 * @brief Change the active input widget
127 * The active input widget influences what is shown in the paint layer
128 * and in the list view: Either a set of placemarks that correspond to
129 * a runner search result or the current route
130 */
131 void setActiveInput(RoutingInputWidget *widget);
132
133 void setupToolBar();
134
135private:
136 void createProgressAnimation();
137 RoutingWidget *m_parent;
138};
139
140RoutingWidgetPrivate::RoutingWidgetPrivate(RoutingWidget *parent, MarbleWidget *marbleWidget)
141 : m_widget(marbleWidget)
142 , m_routingManager(marbleWidget->model()->routingManager())
143 , m_routingLayer(marbleWidget->routingLayer())
144 , m_activeInput(nullptr)
145 , m_inputRequest(nullptr)
146 , m_routingModel(m_routingManager->routingModel())
147 , m_routeRequest(marbleWidget->model()->routingManager()->routeRequest())
148 , m_routeSyncManager(nullptr)
149 , m_zoomRouteAfterDownload(false)
150 , m_document(nullptr)
151 , m_tour(nullptr)
152 , m_playback(nullptr)
153 , m_currentFrame(0)
154 , m_iconSize(16)
155 , m_collapse_width(0)
156 , m_playing(false)
157 , m_planetId(marbleWidget->model()->planetId())
158 , m_toolBar(nullptr)
159 , m_openRouteButton(nullptr)
160 , m_saveRouteButton(nullptr)
161 , m_cloudSyncSeparator(nullptr)
162 , m_uploadToCloudAction(nullptr)
163 , m_openCloudRoutesAction(nullptr)
164 , m_addViaButton(nullptr)
165 , m_reverseRouteButton(nullptr)
166 , m_clearRouteButton(nullptr)
167 , m_configureButton(nullptr)
168 , m_routeUploadDialog(nullptr)
169 , m_parent(parent)
170{
171 createProgressAnimation();
172 m_progressTimer.setInterval(100);
173 if (MarbleGlobal::getInstance()->profiles() & MarbleGlobal::SmallScreen) {
174 m_iconSize = 32;
175 }
176}
177
178void RoutingWidgetPrivate::adjustInputWidgets()
179{
180 for (int i = 0; i < m_inputWidgets.size(); ++i) {
181 m_inputWidgets[i]->setIndex(i);
182 }
183
184 adjustSearchButton();
185}
186
187void RoutingWidgetPrivate::adjustSearchButton()
188{
189 QString text = QObject::tr("Get Directions");
190 QString tooltip = QObject::tr("Retrieve routing instructions for the selected destinations.");
191
192 int validInputs = 0;
193 for (int i = 0; i < m_inputWidgets.size(); ++i) {
194 if (m_inputWidgets[i]->hasTargetPosition()) {
195 ++validInputs;
196 }
197 }
198
199 if (validInputs < 2) {
200 text = QObject::tr("Search");
201 tooltip = QObject::tr("Find places matching the search term");
202 }
203
204 m_ui.searchButton->setText(text);
205 m_ui.searchButton->setToolTip(tooltip);
206}
207
208void RoutingWidgetPrivate::setActiveInput(RoutingInputWidget *widget)
209{
210 Q_ASSERT(widget && "Must not pass null");
211 MarblePlacemarkModel *model = widget->searchResultModel();
212
213 m_activeInput = widget;
214 m_ui.directionsListView->setModel(model);
215 m_routingLayer->setPlacemarkModel(model);
216 m_routingLayer->synchronizeWith(m_ui.directionsListView->selectionModel());
217}
218
219void RoutingWidgetPrivate::setupToolBar()
220{
221 m_toolBar = new QToolBar;
222
223 m_openRouteButton = new QToolButton;
224 m_openRouteButton->setToolTip(QObject::tr("Open Route"));
225 m_openRouteButton->setIcon(QIcon(QStringLiteral(":/icons/16x16/document-open.png")));
226 m_toolBar->addWidget(m_openRouteButton);
227
228 m_saveRouteButton = new QToolButton;
229 m_saveRouteButton->setToolTip(QObject::tr("Save Route"));
230 m_saveRouteButton->setIcon(QIcon(QStringLiteral(":/icons/16x16/document-save.png")));
231 m_toolBar->addWidget(m_saveRouteButton);
232
233 m_playButton = new QToolButton;
234 m_playButton->setToolTip(QObject::tr("Preview Route"));
235 m_playButton->setIcon(QIcon(QStringLiteral(":/marble/playback-play.png")));
236 m_toolBar->addWidget(m_playButton);
237
238 m_cloudSyncSeparator = m_toolBar->addSeparator();
239 m_uploadToCloudAction = m_toolBar->addAction(QObject::tr("Upload to Cloud"));
240 m_uploadToCloudAction->setToolTip(QObject::tr("Upload to Cloud"));
241 m_uploadToCloudAction->setIcon(QIcon(QStringLiteral(":/icons/cloud-upload.png")));
242
243 m_openCloudRoutesAction = m_toolBar->addAction(QObject::tr("Manage Cloud Routes"));
244 m_openCloudRoutesAction->setToolTip(QObject::tr("Manage Cloud Routes"));
245 m_openCloudRoutesAction->setIcon(QIcon(QStringLiteral(":/icons/cloud-download.png")));
246
247 m_toolBar->addSeparator();
248 m_addViaButton = new QToolButton;
249 m_addViaButton->setToolTip(QObject::tr("Add Via"));
250 m_addViaButton->setIcon(QIcon(QStringLiteral(":/marble/list-add.png")));
251 m_toolBar->addWidget(m_addViaButton);
252
253 m_reverseRouteButton = new QToolButton;
254 m_reverseRouteButton->setToolTip(QObject::tr("Reverse Route"));
255 m_reverseRouteButton->setIcon(QIcon(QStringLiteral(":/marble/reverse.png")));
256 m_toolBar->addWidget(m_reverseRouteButton);
257
258 m_clearRouteButton = new QToolButton;
259 m_clearRouteButton->setToolTip(QObject::tr("Clear Route"));
260 m_clearRouteButton->setIcon(QIcon(QStringLiteral(":/marble/edit-clear.png")));
261 m_toolBar->addWidget(m_clearRouteButton);
262
263 m_toolBar->addSeparator();
264
265 m_configureButton = new QToolButton;
266 m_configureButton->setToolTip(QObject::tr("Settings"));
267 m_configureButton->setIcon(QIcon(QStringLiteral(":/icons/16x16/configure.png")));
268 m_toolBar->addWidget(m_configureButton);
269
270 QObject::connect(m_openRouteButton, SIGNAL(clicked()), m_parent, SLOT(openRoute()));
271 QObject::connect(m_saveRouteButton, SIGNAL(clicked()), m_parent, SLOT(saveRoute()));
272 QObject::connect(m_uploadToCloudAction, SIGNAL(triggered()), m_parent, SLOT(uploadToCloud()));
273 QObject::connect(m_openCloudRoutesAction, SIGNAL(triggered()), m_parent, SLOT(openCloudRoutesDialog()));
274 QObject::connect(m_addViaButton, SIGNAL(clicked()), m_parent, SLOT(addInputWidget()));
275 QObject::connect(m_reverseRouteButton, SIGNAL(clicked()), m_routingManager, SLOT(reverseRoute()));
276 QObject::connect(m_clearRouteButton, SIGNAL(clicked()), m_routingManager, SLOT(clearRoute()));
277 QObject::connect(m_configureButton, SIGNAL(clicked()), m_parent, SLOT(configureProfile()));
278 QObject::connect(m_playButton, SIGNAL(clicked()), m_parent, SLOT(toggleRoutePlay()));
279
280 m_toolBar->setIconSize(QSize(16, 16));
281 m_ui.toolBarLayout->addWidget(m_toolBar, 0, Qt::AlignLeft);
282}
283
284void RoutingWidgetPrivate::createProgressAnimation()
285{
286 // Size parameters
287 qreal const h = m_iconSize / 2.0; // Half of the icon size
288 qreal const q = h / 2.0; // Quarter of the icon size
289 qreal const d = 7.5; // Circle diameter
290 qreal const r = d / 2.0; // Circle radius
291
292 // Canvas parameters
293 QImage canvas(m_iconSize, m_iconSize, QImage::Format_ARGB32);
294 QPainter painter(&canvas);
295 painter.setRenderHint(QPainter::Antialiasing, true);
296 painter.setPen(QColor(Qt::gray));
297 painter.setBrush(QColor(Qt::white));
298
299 // Create all frames
300 for (double t = 0.0; t < 2 * M_PI; t += M_PI / 8.0) {
301 canvas.fill(Qt::transparent);
302 QRectF firstCircle(h - r + q * cos(t), h - r + q * sin(t), d, d);
303 QRectF secondCircle(h - r + q * cos(t + M_PI), h - r + q * sin(t + M_PI), d, d);
304 painter.drawEllipse(firstCircle);
305 painter.drawEllipse(secondCircle);
306 m_progressAnimation.push_back(QIcon(QPixmap::fromImage(canvas)));
307 }
308}
309
311 : QWidget(parent)
312 , d(new RoutingWidgetPrivate(this, marbleWidget))
313{
314 d->m_ui.setupUi(this);
315 d->setupToolBar();
316 d->m_ui.routeComboBox->setVisible(false);
317 d->m_ui.routeComboBox->setModel(d->m_routingManager->alternativeRoutesModel());
319
320 d->m_ui.routingProfileComboBox->setModel(d->m_routingManager->profilesModel());
321
322 connect(d->m_routingManager->profilesModel(), SIGNAL(rowsInserted(QModelIndex, int, int)), this, SLOT(selectFirstProfile()));
323 connect(d->m_routingManager->profilesModel(), SIGNAL(modelReset()), this, SLOT(selectFirstProfile()));
324 connect(d->m_routingLayer, SIGNAL(placemarkSelected(QModelIndex)), this, SLOT(activatePlacemark(QModelIndex)));
325 connect(d->m_routingManager, SIGNAL(stateChanged(RoutingManager::State)), this, SLOT(updateRouteState(RoutingManager::State)));
326 connect(d->m_routeRequest, SIGNAL(positionAdded(int)), this, SLOT(insertInputWidget(int)));
327 connect(d->m_routeRequest, SIGNAL(positionRemoved(int)), this, SLOT(removeInputWidget(int)));
328 connect(d->m_routeRequest, SIGNAL(routingProfileChanged()), this, SLOT(updateActiveRoutingProfile()));
329 connect(&d->m_progressTimer, SIGNAL(timeout()), this, SLOT(updateProgress()));
330 connect(d->m_ui.routeComboBox, SIGNAL(currentIndexChanged(int)), d->m_routingManager->alternativeRoutesModel(), SLOT(setCurrentRoute(int)));
331 connect(d->m_routingManager->alternativeRoutesModel(), SIGNAL(currentRouteChanged(int)), d->m_ui.routeComboBox, SLOT(setCurrentIndex(int)));
332 connect(d->m_ui.routingProfileComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(setRoutingProfile(int)));
333 connect(d->m_ui.routingProfileComboBox, SIGNAL(activated(int)), this, SLOT(retrieveRoute()));
334 connect(d->m_routingManager->alternativeRoutesModel(), SIGNAL(rowsInserted(QModelIndex, int, int)), this, SLOT(updateAlternativeRoutes()));
335
336 d->m_ui.directionsListView->setModel(d->m_routingModel);
337
338 QItemSelectionModel *selectionModel = d->m_ui.directionsListView->selectionModel();
339 d->m_routingLayer->synchronizeWith(selectionModel);
340 connect(d->m_ui.directionsListView, SIGNAL(activated(QModelIndex)), this, SLOT(activateItem(QModelIndex)));
341
342 // FIXME: apply for this sector
343 connect(d->m_ui.searchButton, SIGNAL(clicked()), this, SLOT(retrieveRoute()));
344 connect(d->m_ui.showInstructionsButton, SIGNAL(clicked(bool)), this, SLOT(showDirections()));
345
346 for (int i = 0; i < d->m_routeRequest->size(); ++i) {
347 insertInputWidget(i);
348 }
349
350 for (int i = 0; i < 2 && d->m_inputWidgets.size() < 2; ++i) {
351 // Start with source and destination if the route is empty yet
353 }
354 // d->m_ui.descriptionLabel->setVisible( false );
355 d->m_ui.resultLabel->setVisible(false);
357 updateActiveRoutingProfile();
358 updateCloudSyncButtons();
359
360 if (MarbleGlobal::getInstance()->profiles() & MarbleGlobal::SmallScreen) {
361 d->m_ui.directionsListView->setVisible(false);
362 d->m_openRouteButton->setVisible(false);
363 d->m_saveRouteButton->setVisible(false);
364 }
365
366 connect(marbleWidget->model(), SIGNAL(themeChanged(QString)), this, SLOT(handlePlanetChange()));
367}
368
370{
371 delete d->m_playback;
372 delete d->m_tour;
373 if (d->m_document) {
374 d->m_widget->model()->treeModel()->removeDocument(d->m_document);
375 delete d->m_document;
376 }
377 delete d;
378}
379
380void RoutingWidget::retrieveRoute()
381{
382 if (d->m_inputWidgets.size() == 1) {
383 // Search mode
384 d->m_inputWidgets.first()->findPlacemarks();
385 return;
386 }
387
388 int index = d->m_ui.routingProfileComboBox->currentIndex();
389 if (index == -1) {
390 return;
391 }
392 d->m_routeRequest->setRoutingProfile(d->m_routingManager->profilesModel()->profiles().at(index));
393
394 Q_ASSERT(d->m_routeRequest->size() == d->m_inputWidgets.size());
395 for (int i = 0; i < d->m_inputWidgets.size(); ++i) {
396 RoutingInputWidget *widget = d->m_inputWidgets.at(i);
397 if (!widget->hasTargetPosition() && widget->hasInput()) {
398 widget->findPlacemarks();
399 return;
400 }
401 }
402
403 d->m_activeInput = nullptr;
404 if (d->m_routeRequest->size() > 1) {
405 d->m_zoomRouteAfterDownload = true;
406 d->m_routingLayer->setPlacemarkModel(nullptr);
407 d->m_routingManager->retrieveRoute();
408 d->m_ui.directionsListView->setModel(d->m_routingModel);
409 d->m_routingLayer->synchronizeWith(d->m_ui.directionsListView->selectionModel());
410 }
411
412 if (d->m_playback) {
413 d->m_playback->stop();
414 }
415}
416
417void RoutingWidget::activateItem(const QModelIndex &index)
418{
419 QVariant data = index.data(MarblePlacemarkModel::CoordinateRole);
420
421 if (!data.isNull()) {
422 auto position = qvariant_cast<GeoDataCoordinates>(data);
423 d->m_widget->centerOn(position, true);
424 }
425
426 if (d->m_activeInput && index.isValid()) {
427 QVariant data = index.data(MarblePlacemarkModel::CoordinateRole);
428 if (!data.isNull()) {
429 d->m_activeInput->setTargetPosition(data.value<GeoDataCoordinates>(), index.data().toString());
430 }
431 }
432}
433
434void RoutingWidget::handleSearchResult(RoutingInputWidget *widget)
435{
436 d->setActiveInput(widget);
437 MarblePlacemarkModel *model = widget->searchResultModel();
438
439 if (model->rowCount()) {
440 QString const results = tr("placemarks found: %1").arg(model->rowCount());
441 d->m_ui.resultLabel->setText(results);
442 d->m_ui.resultLabel->setVisible(true);
443 // Make sure we have a selection
444 activatePlacemark(model->index(0, 0));
445 } else {
446 QString const results = tr("No placemark found");
447 d->m_ui.resultLabel->setText(QLatin1StringView("<font color=\"red\">") + results + QLatin1StringView("</font>"));
448 d->m_ui.resultLabel->setVisible(true);
449 }
450
451 GeoDataLineString placemarks;
452 for (int i = 0; i < model->rowCount(); ++i) {
453 QVariant data = model->index(i, 0).data(MarblePlacemarkModel::CoordinateRole);
454 if (!data.isNull()) {
455 placemarks << data.value<GeoDataCoordinates>();
456 }
457 }
458
459 if (placemarks.size() > 1) {
460 d->m_widget->centerOn(GeoDataLatLonBox::fromLineString(placemarks));
461 // d->m_ui.descriptionLabel->setVisible( false );
462
463 if (MarbleGlobal::getInstance()->profiles() & MarbleGlobal::SmallScreen) {
464 d->m_ui.directionsListView->setVisible(true);
465 }
466 }
467}
468
469void RoutingWidget::centerOnInputWidget(RoutingInputWidget *widget)
470{
471 if (widget->hasTargetPosition()) {
472 d->m_widget->centerOn(widget->targetPosition());
473 }
474}
475
476void RoutingWidget::activatePlacemark(const QModelIndex &index)
477{
478 if (d->m_activeInput && index.isValid()) {
479 QVariant data = index.data(MarblePlacemarkModel::CoordinateRole);
480 if (!data.isNull()) {
481 d->m_activeInput->setTargetPosition(data.value<GeoDataCoordinates>());
482 }
483 }
484
485 d->m_ui.directionsListView->setCurrentIndex(index);
486}
487
489{
490 d->m_routeRequest->append(GeoDataCoordinates());
491}
492
493void RoutingWidget::insertInputWidget(int index)
494{
495 if (index >= 0 && index <= d->m_inputWidgets.size()) {
496 auto input = new RoutingInputWidget(d->m_widget->model(), index, this);
497 d->m_inputWidgets.insert(index, input);
498 connect(input, SIGNAL(searchFinished(RoutingInputWidget *)), this, SLOT(handleSearchResult(RoutingInputWidget *)));
499 connect(input, SIGNAL(removalRequest(RoutingInputWidget *)), this, SLOT(removeInputWidget(RoutingInputWidget *)));
500 connect(input, SIGNAL(activityRequest(RoutingInputWidget *)), this, SLOT(centerOnInputWidget(RoutingInputWidget *)));
501 connect(input, SIGNAL(mapInputModeEnabled(RoutingInputWidget *, bool)), this, SLOT(requestMapPosition(RoutingInputWidget *, bool)));
502 connect(input, SIGNAL(targetValidityChanged(bool)), this, SLOT(adjustSearchButton()));
503
504 d->m_ui.inputLayout->insertWidget(index, input);
505 d->adjustInputWidgets();
506 }
507}
508
509void RoutingWidget::removeInputWidget(RoutingInputWidget *widget)
510{
511 int index = d->m_inputWidgets.indexOf(widget);
512 if (index >= 0) {
513 if (d->m_inputWidgets.size() < 3) {
514 widget->clear();
515 } else {
516 d->m_routeRequest->remove(index);
517 }
518 d->m_routingManager->retrieveRoute();
519 }
520}
521
522void RoutingWidget::removeInputWidget(int index)
523{
524 if (index >= 0 && index < d->m_inputWidgets.size()) {
525 RoutingInputWidget *widget = d->m_inputWidgets.at(index);
526 d->m_inputWidgets.remove(index);
527 d->m_ui.inputLayout->removeWidget(widget);
528 widget->deleteLater();
529 if (widget == d->m_activeInput) {
530 d->m_activeInput = nullptr;
531 d->m_routingLayer->setPlacemarkModel(nullptr);
532 d->m_ui.directionsListView->setModel(d->m_routingModel);
533 d->m_routingLayer->synchronizeWith(d->m_ui.directionsListView->selectionModel());
534 }
535 d->adjustInputWidgets();
536 }
537
538 if (d->m_inputWidgets.size() < 2) {
540 }
541}
542
543void RoutingWidget::updateRouteState(RoutingManager::State state)
544{
545 clearTour();
546
547 switch (state) {
548 case RoutingManager::Downloading:
549 d->m_ui.routeComboBox->setVisible(false);
550 d->m_ui.routeComboBox->clear();
551 d->m_progressTimer.start();
552 d->m_ui.resultLabel->setVisible(false);
553 break;
554 case RoutingManager::Retrieved: {
555 d->m_progressTimer.stop();
556 d->m_ui.searchButton->setIcon(QIcon());
557 if (d->m_routingManager->routingModel()->rowCount() == 0) {
558 const QString results = tr("No route found");
559 d->m_ui.resultLabel->setText(QLatin1StringView("<font color=\"red\">") + results + QLatin1StringView("</font>"));
560 d->m_ui.resultLabel->setVisible(true);
561 }
562 } break;
563 }
564
565 d->m_saveRouteButton->setEnabled(d->m_routingManager->routingModel()->rowCount() > 0);
566}
567
568void RoutingWidget::requestMapPosition(RoutingInputWidget *widget, bool enabled)
569{
570 pointSelectionCanceled();
571
572 if (enabled) {
573 d->m_inputRequest = widget;
574 d->m_widget->installEventFilter(this);
575 d->m_widget->setFocus(Qt::OtherFocusReason);
576 }
577}
578
579void RoutingWidget::retrieveSelectedPoint(const GeoDataCoordinates &coordinates)
580{
581 if (d->m_inputRequest && d->m_inputWidgets.contains(d->m_inputRequest)) {
582 d->m_inputRequest->setTargetPosition(coordinates);
583 d->m_widget->update();
584 }
585
586 d->m_inputRequest = nullptr;
587 d->m_widget->removeEventFilter(this);
588}
589
590void RoutingWidget::adjustSearchButton()
591{
592 d->adjustSearchButton();
593}
594
595void RoutingWidget::pointSelectionCanceled()
596{
597 if (d->m_inputRequest && d->m_inputWidgets.contains(d->m_inputRequest)) {
598 d->m_inputRequest->abortMapInputRequest();
599 }
600
601 d->m_inputRequest = nullptr;
602 d->m_widget->removeEventFilter(this);
603}
604
605void RoutingWidget::configureProfile()
606{
607 int index = d->m_ui.routingProfileComboBox->currentIndex();
608 if (index != -1) {
609 RoutingProfileSettingsDialog dialog(d->m_widget->model()->pluginManager(), d->m_routingManager->profilesModel(), this);
610 dialog.editProfile(d->m_ui.routingProfileComboBox->currentIndex());
611 d->m_routeRequest->setRoutingProfile(d->m_routingManager->profilesModel()->profiles().at(index));
612 }
613}
614
615void RoutingWidget::updateProgress()
616{
617 if (!d->m_progressAnimation.isEmpty()) {
618 d->m_currentFrame = (d->m_currentFrame + 1) % d->m_progressAnimation.size();
619 QIcon frame = d->m_progressAnimation[d->m_currentFrame];
620 d->m_ui.searchButton->setIcon(frame);
621 }
622}
623
624void RoutingWidget::updateAlternativeRoutes()
625{
626 if (d->m_ui.routeComboBox->count() == 1) {
627 // Parts of the route may lie outside the route trip points
628 GeoDataLatLonBox const bbox = d->m_routingManager->routingModel()->route().bounds();
629 if (d->m_zoomRouteAfterDownload) {
630 d->m_zoomRouteAfterDownload = false;
631 d->m_widget->centerOn(bbox);
632 }
633 }
634
635 d->m_ui.routeComboBox->setVisible(d->m_ui.routeComboBox->count() > 0);
636 if (d->m_ui.routeComboBox->currentIndex() < 0 && d->m_ui.routeComboBox->count() > 0) {
637 d->m_ui.routeComboBox->setCurrentIndex(0);
638 }
639
640 QString const results = tr("routes found: %1").arg(d->m_ui.routeComboBox->count());
641 d->m_ui.resultLabel->setText(results);
642 d->m_ui.resultLabel->setVisible(true);
643 d->m_saveRouteButton->setEnabled(d->m_routingManager->routingModel()->rowCount() > 0);
644}
645
647{
648 d->m_ui.showInstructionsButton->setVisible(visible);
649}
650
651void RoutingWidget::setRouteSyncManager(RouteSyncManager *manager)
652{
653 d->m_routeSyncManager = manager;
654 connect(d->m_routeSyncManager, SIGNAL(routeSyncEnabledChanged(bool)), this, SLOT(updateCloudSyncButtons()));
655 updateCloudSyncButtons();
656}
657
659{
660 QString const file = QFileDialog::getOpenFileName(this, tr("Open Route"), d->m_routingManager->lastOpenPath(), tr("KML Files (*.kml)"));
661 if (!file.isEmpty()) {
662 d->m_routingManager->setLastOpenPath(QFileInfo(file).absolutePath());
663 d->m_zoomRouteAfterDownload = true;
664 d->m_routingManager->loadRoute(file);
665 updateAlternativeRoutes();
666 }
667}
668
669void RoutingWidget::selectFirstProfile()
670{
671 int count = d->m_routingManager->profilesModel()->rowCount();
672 if (count && d->m_ui.routingProfileComboBox->currentIndex() < 0) {
673 d->m_ui.routingProfileComboBox->setCurrentIndex(0);
674 }
675}
676
677void RoutingWidget::setRoutingProfile(int index)
678{
679 if (index >= 0 && index < d->m_routingManager->profilesModel()->rowCount()) {
680 d->m_routeRequest->setRoutingProfile(d->m_routingManager->profilesModel()->profiles().at(index));
681 }
682}
683
684void RoutingWidget::showDirections()
685{
686 d->m_ui.directionsListView->setVisible(true);
687}
688
690{
692 tr("Save Route"), // krazy:exclude=qclasses
693 d->m_routingManager->lastSavePath(),
694 tr("KML files (*.kml)"));
695
696 if (!fileName.isEmpty()) {
697 // maemo 5 file dialog does not append the file extension
698 if (!fileName.endsWith(QLatin1StringView(".kml"), Qt::CaseInsensitive)) {
699 fileName += QLatin1StringView(".kml");
700 }
701 d->m_routingManager->setLastSavePath(QFileInfo(fileName).absolutePath());
702 d->m_routingManager->saveRoute(fileName);
703 }
704}
705
707{
708 Q_ASSERT(d->m_routeSyncManager);
709
710 if (!d->m_routeUploadDialog) {
711 d->m_routeUploadDialog = new QProgressDialog(d->m_widget);
712 d->m_routeUploadDialog->setWindowTitle(tr("Uploading route..."));
713 d->m_routeUploadDialog->setMinimum(0);
714 d->m_routeUploadDialog->setMaximum(100);
715 d->m_routeUploadDialog->setAutoClose(true);
716 d->m_routeUploadDialog->setAutoReset(true);
717 connect(d->m_routeSyncManager, SIGNAL(routeUploadProgress(qint64, qint64)), this, SLOT(updateUploadProgress(qint64, qint64)));
718 }
719
720 d->m_routeUploadDialog->show();
721 d->m_routeSyncManager->uploadRoute();
722}
723
725{
726 Q_ASSERT(d->m_routeSyncManager);
727 d->m_routeSyncManager->prepareRouteList();
728
729 QPointer<CloudRoutesDialog> dialog = new CloudRoutesDialog(d->m_routeSyncManager->model(), d->m_widget);
730 connect(d->m_routeSyncManager, SIGNAL(routeListDownloadProgress(qint64, qint64)), dialog, SLOT(updateListDownloadProgressbar(qint64, qint64)));
731 connect(dialog, SIGNAL(downloadButtonClicked(QString)), d->m_routeSyncManager, SLOT(downloadRoute(QString)));
732 connect(dialog, SIGNAL(openButtonClicked(QString)), this, SLOT(openCloudRoute(QString)));
733 connect(dialog, SIGNAL(deleteButtonClicked(QString)), d->m_routeSyncManager, SLOT(deleteRoute(QString)));
734 connect(dialog, SIGNAL(removeFromCacheButtonClicked(QString)), d->m_routeSyncManager, SLOT(removeRouteFromCache(QString)));
735 connect(dialog, SIGNAL(uploadToCloudButtonClicked(QString)), d->m_routeSyncManager, SLOT(uploadRoute(QString)));
736 dialog->exec();
737 delete dialog;
738}
739
740void RoutingWidget::updateActiveRoutingProfile()
741{
742 RoutingProfile const profile = d->m_routingManager->routeRequest()->routingProfile();
743 QList<RoutingProfile> const profiles = d->m_routingManager->profilesModel()->profiles();
744 d->m_ui.routingProfileComboBox->setCurrentIndex(profiles.indexOf(profile));
745}
746
747void RoutingWidget::updateCloudSyncButtons()
748{
749 bool const show = d->m_routeSyncManager && d->m_routeSyncManager->isRouteSyncEnabled();
750 d->m_cloudSyncSeparator->setVisible(show);
751 d->m_uploadToCloudAction->setVisible(show);
752 d->m_openCloudRoutesAction->setVisible(show);
753}
754
755void RoutingWidget::openCloudRoute(const QString &identifier)
756{
757 Q_ASSERT(d->m_routeSyncManager);
758 d->m_routeSyncManager->openRoute(identifier);
759 d->m_widget->centerOn(d->m_routingManager->routingModel()->route().bounds());
760}
761
762void RoutingWidget::updateUploadProgress(qint64 sent, qint64 total)
763{
764 Q_ASSERT(d->m_routeUploadDialog);
765 d->m_routeUploadDialog->setValue(100.0 * sent / total);
766}
767
768bool RoutingWidget::eventFilter(QObject *o, QEvent *event)
769{
770 if (o != d->m_widget) {
771 return QWidget::eventFilter(o, event);
772 }
773
774 Q_ASSERT(d->m_inputRequest != nullptr);
775 Q_ASSERT(d->m_inputWidgets.contains(d->m_inputRequest));
776
777 if (event->type() == QEvent::MouseButtonPress) {
778 auto e = static_cast<QMouseEvent *>(event);
779 return e->button() == Qt::LeftButton;
780 }
781
782 if (event->type() == QEvent::MouseButtonRelease) {
783 auto e = static_cast<QMouseEvent *>(event);
784 qreal lon(0.0), lat(0.0);
785 if (e->button() == Qt::LeftButton && d->m_widget->geoCoordinates(e->pos().x(), e->pos().y(), lon, lat, GeoDataCoordinates::Radian)) {
786 retrieveSelectedPoint(GeoDataCoordinates(lon, lat));
787 return true;
788 } else {
789 return QWidget::eventFilter(o, event);
790 }
791 }
792
793 if (event->type() == QEvent::MouseMove) {
794 d->m_widget->setCursor(Qt::CrossCursor);
795 return true;
796 }
797
798 if (event->type() == QEvent::KeyPress) {
799 auto e = static_cast<QKeyEvent *>(event);
800 if (e->key() == Qt::Key_Escape) {
801 pointSelectionCanceled();
802 return true;
803 }
804
805 return QWidget::eventFilter(o, event);
806 }
807
808 return QWidget::eventFilter(o, event);
809}
810
811void RoutingWidget::resizeEvent(QResizeEvent *e)
812{
814}
815
816void RoutingWidget::toggleRoutePlay()
817{
818 if (!d->m_playback) {
819 if (d->m_routingModel->rowCount() != 0) {
820 initializeTour();
821 }
822 }
823
824 if (!d->m_playback)
825 return;
826
827 if (!d->m_playing) {
828 d->m_playing = true;
829 d->m_playButton->setIcon(QIcon(QStringLiteral(":/marble/playback-pause.png")));
830
831 if (d->m_playback) {
832 d->m_playback->play();
833 }
834 } else {
835 d->m_playing = false;
836 d->m_playButton->setIcon(QIcon(QStringLiteral(":/marble/playback-play.png")));
837 d->m_playback->pause();
838 }
839}
840
841void RoutingWidget::initializeTour()
842{
843 d->m_tour = new GeoDataTour;
844 if (d->m_document) {
845 d->m_widget->model()->treeModel()->removeDocument(d->m_document);
846 delete d->m_document;
847 }
848 d->m_document = new GeoDataDocument;
849 d->m_document->setId(QStringLiteral("tourdoc"));
850 d->m_document->append(d->m_tour);
851
852 d->m_tour->setPlaylist(new GeoDataPlaylist);
853 Route const route = d->m_widget->model()->routingManager()->routingModel()->route();
854 GeoDataLineString path = route.path();
855 if (path.size() < 1) {
856 return;
857 }
858
859 QList<WaypointInfo> waypoints;
860 double totalDistance = 0.0;
861 for (int i = 0; i < route.size(); ++i) {
862 // TODO: QString( i )?
863 waypoints << WaypointInfo(i, totalDistance, route.at(i).path().first(), route.at(i).maneuver(), QLatin1StringView("start ") + QString::number(i));
864 totalDistance += route.at(i).distance();
865 }
866
867 if (waypoints.size() < 1) {
868 return;
869 }
870
871 QList<WaypointInfo> const allWaypoints = waypoints;
872 totalDistance = 0.0;
873 GeoDataCoordinates last = path.at(0);
874 int j = 0; // next waypoint
875 qreal planetRadius = d->m_widget->model()->planet()->radius();
876 for (int i = 1; i < path.size(); ++i) {
877 GeoDataCoordinates coordinates = path.at(i);
878 totalDistance += planetRadius * path.at(i - 1).sphericalDistanceTo(coordinates); // Distance to route start
879 while (totalDistance >= allWaypoints[j].distance && j + 1 < allWaypoints.size()) {
880 ++j;
881 }
882 int const lastIndex = qBound(0, j - 1, allWaypoints.size() - 1); // previous waypoint
883 double const lastDistance = qAbs(totalDistance - allWaypoints[lastIndex].distance);
884 double const nextDistance = qAbs(allWaypoints[j].distance - totalDistance);
885 double const waypointDistance = qMin(lastDistance, nextDistance); // distance to closest waypoint
886 double const step = qBound(100.0, waypointDistance * 2, 1000.0); // support point distance (higher density close to waypoints)
887
888 double const distance = planetRadius * last.sphericalDistanceTo(coordinates);
889 if (i > 1 && distance < step) {
890 continue;
891 }
892 last = coordinates;
893
894 auto lookat = new GeoDataLookAt;
895 // Choose a zoom distance of 400, 600 or 800 meters based on the distance to the closest waypoint
896 double const range = waypointDistance < 400 ? 400 : (waypointDistance < 2000 ? 600 : 800);
897 coordinates.setAltitude(range);
898 lookat->setCoordinates(coordinates);
899 lookat->setRange(range);
900 auto flyto = new GeoDataFlyTo;
901 double const duration = 0.75;
902 flyto->setDuration(duration);
903 flyto->setView(lookat);
904 flyto->setFlyToMode(GeoDataFlyTo::Smooth);
905 d->m_tour->playlist()->addPrimitive(flyto);
906
907 if (!waypoints.empty() && totalDistance > waypoints.first().distance - 100) {
908 WaypointInfo const waypoint = waypoints.first();
909 waypoints.pop_front();
910 auto updateCreate = new GeoDataAnimatedUpdate;
911 updateCreate->setUpdate(new GeoDataUpdate);
912 updateCreate->update()->setCreate(new GeoDataCreate);
913 auto placemarkCreate = new GeoDataPlacemark;
914 QString const waypointId = QStringLiteral("waypoint-%1").arg(i, 0, 10);
915 placemarkCreate->setId(waypointId);
916 placemarkCreate->setTargetId(d->m_document->id());
917 placemarkCreate->setCoordinate(waypoint.coordinates);
918 GeoDataStyle::Ptr style(new GeoDataStyle);
919 style->iconStyle().setIconPath(waypoint.maneuver.directionPixmap());
920 placemarkCreate->setStyle(style);
921 updateCreate->update()->create()->append(placemarkCreate);
922 d->m_tour->playlist()->addPrimitive(updateCreate);
923
924 auto updateDelete = new GeoDataAnimatedUpdate;
925 updateDelete->setDelayedStart(2);
926 updateDelete->setUpdate(new GeoDataUpdate);
927 updateDelete->update()->setDelete(new GeoDataDelete);
928 auto placemarkDelete = new GeoDataPlacemark;
929 placemarkDelete->setTargetId(waypointId);
930 updateDelete->update()->getDelete()->append(placemarkDelete);
931 d->m_tour->playlist()->addPrimitive(updateDelete);
932 }
933 }
934
935 d->m_playback = new TourPlayback;
936 d->m_playback->setMarbleWidget(d->m_widget);
937 d->m_playback->setTour(d->m_tour);
938 d->m_widget->model()->treeModel()->addDocument(d->m_document);
939 QObject::connect(d->m_playback, SIGNAL(finished()), this, SLOT(seekTourToStart()));
940}
941
942void RoutingWidget::centerOn(const GeoDataCoordinates &coordinates)
943{
944 if (d->m_widget) {
945 GeoDataLookAt lookat;
946 lookat.setCoordinates(coordinates);
947 lookat.setRange(coordinates.altitude());
948 d->m_widget->flyTo(lookat, Instant);
949 }
950}
951
952void RoutingWidget::clearTour()
953{
954 d->m_playing = false;
955 d->m_playButton->setIcon(QIcon(QStringLiteral(":/marble/playback-play.png")));
956 delete d->m_playback;
957 d->m_playback = nullptr;
958 if (d->m_document) {
959 d->m_widget->model()->treeModel()->removeDocument(d->m_document);
960 delete d->m_document;
961 d->m_document = nullptr;
962 d->m_tour = nullptr;
963 }
964}
965
966void RoutingWidget::seekTourToStart()
967{
968 Q_ASSERT(d->m_playback);
969 d->m_playback->stop();
970 d->m_playback->seek(0);
971 d->m_playButton->setIcon(QIcon(QStringLiteral(":/marble/playback-play.png")));
972 d->m_playing = false;
973}
974
975void RoutingWidget::handlePlanetChange()
976{
977 const QString newPlanetId = d->m_widget->model()->planetId();
978
979 if (newPlanetId == d->m_planetId) {
980 return;
981 }
982
983 d->m_planetId = newPlanetId;
984 d->m_routingManager->clearRoute();
985}
986
987} // namespace Marble
988
989#include "moc_RoutingWidget.cpp"
This file contains the headers for MarbleModel.
This file contains the headers for MarbleWidget.
void setAltitude(const qreal altitude)
set the altitude of the Point in meters
qreal altitude() const
return the altitude of the Point in meters
qreal sphericalDistanceTo(const GeoDataCoordinates &other) const
This method calculates the shortest distance between two points on a sphere.
A 3d point representation.
static GeoDataLatLonBox fromLineString(const GeoDataLineString &lineString)
Create the smallest bounding box from a line string.
@ CoordinateRole
The GeoDataCoordinates coordinate.
A widget class that displays a view of the earth.
MarbleModel * model()
Return the model that this view shows.
Combines a line edit for input and a couple of buttons to let the user type in a search term,...
A widget consisting of input fields for places / routing destinations, a list view showing routing in...
void setShowDirectionsButtonVisible(bool visible)
Show or hide the "open file..." button.
void openCloudRoutesDialog()
Open cloud routes dialog.
~RoutingWidget() override
Destructor.
RoutingWidget(MarbleWidget *marbleWidget, QWidget *parent)
Constructor.
void addInputWidget()
Add another input field at the end.
void openRoute()
Ask the user for a kml file to open.
void uploadToCloud()
Upload route to the cloud.
void saveRoute()
Ask the user for a kml file to save the current route to.
QString path(const QString &relativePath)
Binds a QML item to a specific geodetic location in screen coordinates.
@ Instant
Change camera position immediately (no interpolation)
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
MouseButtonPress
QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, Options options)
QString getSaveFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, Options options)
bool empty() const const
T & first()
qsizetype indexOf(const AT &value, qsizetype from) const const
void pop_front()
qsizetype size() const const
QVariant data(int role) const const
bool isValid() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
virtual bool event(QEvent *e)
virtual bool eventFilter(QObject *watched, QEvent *event)
QObject * parent() const const
QString tr(const char *sourceText, const char *disambiguation, int n)
QPixmap fromImage(QImage &&image, Qt::ImageConversionFlags flags)
QString arg(Args &&... args) const const
const QChar at(qsizetype position) const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString number(double n, char format, int precision)
qsizetype size() const const
AlignLeft
CaseInsensitive
CrossCursor
OtherFocusReason
Key_Escape
LeftButton
void * data()
bool isNull() const const
QString toString() const const
T value() const const
QWidget(QWidget *parent, Qt::WindowFlags f)
QLayout * layout() const const
virtual void resizeEvent(QResizeEvent *event)
void setContentsMargins(const QMargins &margins)
void show()
QStyle * style() const const
void setToolTip(const QString &)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 24 2025 11:52:10 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.