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
310RoutingWidget::RoutingWidget(MarbleWidget *marbleWidget, QWidget *parent)
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{
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()) {
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()) {
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.
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,...
void setShowDirectionsButtonVisible(bool visible)
Show or hide the "open file..." button.
void openCloudRoutesDialog()
Open cloud routes dialog.
~RoutingWidget() override
Destructor.
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 eventFilter(QObject *watched, QEvent *event)
QString tr(const char *sourceText, const char *disambiguation, int n)
QPixmap fromImage(QImage &&image, Qt::ImageConversionFlags flags)
Qt::MouseButton button() const const
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
virtual bool event(QEvent *event) override
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-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:15:46 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.