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