1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2010 Jens-Michael Hoffmann <jmho@c-xx.com>
4 //
5 
6 #include "DownloadRegionDialog.h"
7 
8 #include <cmath>
9 
10 #include <QDialogButtonBox>
11 #include <QGroupBox>
12 #include <QHBoxLayout>
13 #include <QHideEvent>
14 #include <QLabel>
15 #include <QPushButton>
16 #include <QRadioButton>
17 #include <QShowEvent>
18 #include <QVBoxLayout>
19 #include <QSpinBox>
20 #include <QScrollArea>
21 #include <QSet>
22 
23 #include "GeoDataLatLonAltBox.h"
24 #include "MarbleDebug.h"
25 #include "MarbleModel.h"
26 #include "MarbleWidget.h"
27 #include "LatLonBoxWidget.h"
28 #include "TextureLayer.h"
29 #include "TileId.h"
30 #include "TileCoordsPyramid.h"
31 #include "TileLevelRangeWidget.h"
32 #include "TileLoaderHelper.h"
33 #include "routing/RoutingManager.h"
34 #include "routing/RoutingModel.h"
35 #include "GeoDataCoordinates.h"
36 #include "GeoDataLineString.h"
37 #include "DownloadRegion.h"
38 #include "GeoSceneDocument.h"
39 #include "GeoSceneMap.h"
40 #include "Route.h"
41 
42 namespace Marble
43 {
44 
45 int const maxTilesCount = 100000;
46 int const minimumRouteOffset = 0;
47 int const maximumRouteOffset = 10000;
48 int averageTileSize = 13; //The average size of a tile in kilobytes
49 
50 class Q_DECL_HIDDEN DownloadRegionDialog::Private
51 {
52 public:
53     Private( MarbleWidget *const widget, QDialog * const dialog );
54     QWidget * createSelectionMethodBox();
55     QLayout * createTilesCounter();
56     QWidget * createOkCancelButtonBox();
57 
58     bool hasRoute() const;
59     bool hasTextureLayer() const;
60     QDialog * m_dialog;
61     QRadioButton * m_visibleRegionMethodButton;
62     QRadioButton * m_specifiedRegionMethodButton;
63     LatLonBoxWidget * m_latLonBoxWidget;
64     TileLevelRangeWidget * m_tileLevelRangeWidget;
65     QRadioButton *m_routeDownloadMethodButton;
66     QLabel* m_routeOffsetLabel;
67     QDoubleSpinBox *m_routeOffsetSpinBox;
68     QLabel * m_tilesCountLabel;
69     QLabel * m_tileSizeInfo;
70     QPushButton * m_okButton;
71     QPushButton * m_applyButton;
72     TextureLayer const * m_textureLayer;
73     int m_visibleTileLevel;
74     MarbleModel const*const m_model;
75     MarbleWidget *const m_widget;
76     SelectionMethod m_selectionMethod;
77     GeoDataLatLonAltBox m_visibleRegion;
78     RoutingModel *m_routingModel;
79     DownloadRegion m_downloadRegion;
80 };
81 
Private(MarbleWidget * const widget,QDialog * const dialog)82 DownloadRegionDialog::Private::Private( MarbleWidget * const widget,
83                                         QDialog * const dialog )
84     : m_dialog( dialog ),
85       m_visibleRegionMethodButton( nullptr ),
86       m_specifiedRegionMethodButton( nullptr ),
87       m_latLonBoxWidget( new LatLonBoxWidget ),
88       m_tileLevelRangeWidget( new TileLevelRangeWidget ),
89       m_routeDownloadMethodButton( nullptr ),
90       m_routeOffsetLabel( nullptr ),
91       m_routeOffsetSpinBox( nullptr ),
92       m_tilesCountLabel( nullptr ),
93       m_tileSizeInfo( nullptr ),
94       m_okButton( nullptr ),
95       m_applyButton( nullptr ),
96       m_textureLayer( widget->textureLayer() ),
97       m_visibleTileLevel( m_textureLayer->tileZoomLevel() ),
98       m_model( widget->model() ),
99       m_widget( widget ),
100       m_selectionMethod( VisibleRegionMethod ),
101       m_visibleRegion(),
102       m_routingModel( widget->model()->routingManager()->routingModel() )
103 {
104     m_latLonBoxWidget->setEnabled( false );
105     m_latLonBoxWidget->setLatLonBox( m_visibleRegion );
106     m_tileLevelRangeWidget->setDefaultLevel( m_visibleTileLevel );
107     m_downloadRegion.setMarbleModel( widget->model() );
108 }
109 
createSelectionMethodBox()110 QWidget * DownloadRegionDialog::Private::createSelectionMethodBox()
111 {
112     m_visibleRegionMethodButton = new QRadioButton( tr( "Visible region" ) );
113     m_specifiedRegionMethodButton = new QRadioButton( tr( "Specify region" ) );
114 
115     m_routeDownloadMethodButton = new QRadioButton( tr( "Download Route" ) );
116     m_routeDownloadMethodButton->setToolTip( tr( "Enabled when a route exists" ) );
117     m_routeDownloadMethodButton->setEnabled( hasRoute() );
118     m_routeDownloadMethodButton->setChecked( hasRoute() );
119     m_routeOffsetSpinBox = new QDoubleSpinBox();
120     m_routeOffsetSpinBox->setEnabled( hasRoute() );
121     m_routeOffsetSpinBox->setRange( minimumRouteOffset, maximumRouteOffset );
122     int defaultOffset = 500;
123     m_routeOffsetSpinBox->setValue( defaultOffset );
124     m_routeOffsetSpinBox->setSingleStep( 100 );
125     m_routeOffsetSpinBox->setSuffix( " m" );
126     m_routeOffsetSpinBox->setDecimals( 0 );
127     m_routeOffsetSpinBox->setAlignment( Qt::AlignRight );
128 
129     m_routeOffsetLabel = new QLabel( tr( "Offset from route:" ) );
130     m_routeOffsetLabel->setAlignment( Qt::AlignHCenter );
131 
132     connect( m_visibleRegionMethodButton, SIGNAL(toggled(bool)),
133              m_dialog, SLOT(toggleSelectionMethod()) );
134     connect( m_specifiedRegionMethodButton, SIGNAL(toggled(bool)),
135              m_dialog, SLOT(toggleSelectionMethod()));
136     connect( m_routeDownloadMethodButton, SIGNAL(toggled(bool)),
137              m_dialog, SLOT(toggleSelectionMethod()) );
138     connect( m_routingModel, SIGNAL(modelReset()), m_dialog, SLOT(updateRouteDialog()) );
139     connect( m_routingModel, SIGNAL(rowsInserted(QModelIndex,int,int)),
140              m_dialog, SLOT(updateRouteDialog()) );
141     connect( m_routingModel, SIGNAL(rowsRemoved(QModelIndex,int,int)),
142              m_dialog, SLOT(updateRouteDialog()) );
143 
144     QHBoxLayout *routeOffsetLayout = new QHBoxLayout;
145     routeOffsetLayout->addWidget( m_routeOffsetLabel );
146     routeOffsetLayout->insertSpacing( 0, 25 );
147     routeOffsetLayout->addWidget( m_routeOffsetSpinBox );
148 
149     QVBoxLayout * const routeLayout = new QVBoxLayout;
150     routeLayout->addWidget( m_routeDownloadMethodButton );
151     routeLayout->addLayout( routeOffsetLayout );
152 
153     QVBoxLayout * const layout = new QVBoxLayout;
154     layout->addWidget( m_visibleRegionMethodButton );
155     layout->addLayout( routeLayout );
156     layout->addWidget( m_specifiedRegionMethodButton );
157     layout->addWidget( m_latLonBoxWidget );
158 
159     bool const smallScreen = MarbleGlobal::getInstance()->profiles() & MarbleGlobal::SmallScreen;
160     m_specifiedRegionMethodButton->setVisible( !smallScreen );
161     m_latLonBoxWidget->setVisible( !smallScreen );
162 
163     if ( smallScreen ) {
164         QWidget * const selectionMethodWidget = new QWidget;
165         selectionMethodWidget->setLayout( layout );
166         return selectionMethodWidget;
167     } else {
168         QGroupBox * const selectionMethodBox = new QGroupBox( tr( "Selection Method" ) );
169         selectionMethodBox->setLayout( layout );
170         return selectionMethodBox;
171     }
172 }
173 
createTilesCounter()174 QLayout * DownloadRegionDialog::Private::createTilesCounter()
175 {
176     QLabel * const description = new QLabel( tr( "Number of tiles to download:" ) );
177     m_tilesCountLabel = new QLabel;
178     m_tileSizeInfo = new QLabel;
179 
180     QHBoxLayout * const tilesCountLayout = new QHBoxLayout;
181     tilesCountLayout->addWidget( description );
182     tilesCountLayout->addWidget( m_tilesCountLabel );
183     //tilesCountLayout->insertSpacing( 0, 5 );
184     QVBoxLayout * const layout = new QVBoxLayout;
185     layout->addLayout( tilesCountLayout );
186     layout->addWidget( m_tileSizeInfo );
187     return layout;
188 }
189 
createOkCancelButtonBox()190 QWidget * DownloadRegionDialog::Private::createOkCancelButtonBox()
191 {
192     QDialogButtonBox * const buttonBox = new QDialogButtonBox;
193     m_okButton = buttonBox->addButton( QDialogButtonBox::Ok );
194     m_applyButton = buttonBox->addButton( QDialogButtonBox::Apply );
195     if ( MarbleGlobal::getInstance()->profiles() & MarbleGlobal::SmallScreen ) {
196         buttonBox->removeButton( m_applyButton );
197         m_applyButton->setVisible( false );
198     }
199     buttonBox->addButton( QDialogButtonBox::Cancel );
200     connect( buttonBox, SIGNAL(accepted()), m_dialog, SLOT(accept()) );
201     connect( buttonBox, SIGNAL(rejected()), m_dialog, SLOT(reject()) );
202     connect( m_applyButton, SIGNAL(clicked()), m_dialog, SIGNAL(applied()) );
203     return buttonBox;
204 }
205 
hasRoute() const206 bool DownloadRegionDialog::Private::hasRoute() const
207 {
208     return !m_routingModel->route().path().isEmpty();
209 }
210 
hasTextureLayer() const211 bool DownloadRegionDialog::Private::hasTextureLayer() const
212 {
213     return m_model->mapTheme()->map()->hasTextureLayers();
214 }
215 
DownloadRegionDialog(MarbleWidget * const widget,QWidget * const parent,Qt::WindowFlags const f)216 DownloadRegionDialog::DownloadRegionDialog( MarbleWidget *const widget, QWidget * const parent,
217                                             Qt::WindowFlags const f )
218     : QDialog( parent, f ),
219       d( new Private( widget, this ))
220 {
221     setWindowTitle( tr( "Download Region" ));
222     QVBoxLayout * const layout = new QVBoxLayout;
223     layout->addWidget( d->createSelectionMethodBox() );
224     layout->addWidget( d->m_tileLevelRangeWidget );
225     layout->addLayout( d->createTilesCounter() );
226 
227     if ( MarbleGlobal::getInstance()->profiles() & MarbleGlobal::SmallScreen ) {
228         QWidget* widget = new QWidget( this );
229         widget->setLayout( layout );
230         QScrollArea* scrollArea = new QScrollArea( this );
231         scrollArea->setFrameShape( QFrame::NoFrame );
232         scrollArea->setWidget( widget );
233         QVBoxLayout * const mainLayout = new QVBoxLayout;
234         mainLayout->addWidget( scrollArea );
235         mainLayout->addWidget( d->createOkCancelButtonBox() );
236         setLayout( mainLayout );
237     } else {
238         layout->addWidget( d->createOkCancelButtonBox() );
239         setLayout( layout );
240     }
241 
242     connect( d->m_latLonBoxWidget, SIGNAL(valueChanged()), SLOT(updateTilesCount()) );
243     connect( d->m_tileLevelRangeWidget, SIGNAL(topLevelChanged(int)),
244              SLOT(updateTilesCount()) );
245     connect( d->m_tileLevelRangeWidget, SIGNAL(bottomLevelChanged(int)),
246              SLOT(updateTilesCount()) );
247     connect( d->m_routeOffsetSpinBox, SIGNAL(valueChanged(double)), SLOT(updateTilesCount()) );
248     connect( d->m_routeOffsetSpinBox, SIGNAL(valueChanged(double)), SLOT(setOffsetUnit()) );
249     connect( d->m_model, SIGNAL(themeChanged(QString)), SLOT(updateTilesCount()) );
250 }
251 
~DownloadRegionDialog()252 DownloadRegionDialog::~DownloadRegionDialog()
253 {
254     delete d;
255 }
256 
setAllowedTileLevelRange(int const minimumTileLevel,int const maximumTileLevel)257 void DownloadRegionDialog::setAllowedTileLevelRange( int const minimumTileLevel,
258                                                      int const maximumTileLevel )
259 {
260     d->m_tileLevelRangeWidget->setAllowedLevelRange( minimumTileLevel, maximumTileLevel );
261 }
262 
setVisibleTileLevel(int const tileLevel)263 void DownloadRegionDialog::setVisibleTileLevel( int const tileLevel )
264 {
265     d->m_visibleTileLevel = tileLevel;
266     d->m_tileLevelRangeWidget->setDefaultLevel( tileLevel );
267     d->m_downloadRegion.setVisibleTileLevel( tileLevel );
268 }
269 
setSelectionMethod(SelectionMethod const selectionMethod)270 void DownloadRegionDialog::setSelectionMethod( SelectionMethod const selectionMethod )
271 {
272     // block signals to prevent infinite recursion:
273     // radioButton->setChecked() -> toggleSelectionMethod() -> setSelectionMethod()
274     //     -> radioButton->setChecked() -> ...
275     d->m_visibleRegionMethodButton->blockSignals( true );
276     d->m_specifiedRegionMethodButton->blockSignals( true );
277     d->m_routeDownloadMethodButton->blockSignals( true );
278 
279     d->m_selectionMethod = selectionMethod;
280     switch ( selectionMethod ) {
281     case VisibleRegionMethod:
282         d->m_visibleRegionMethodButton->setChecked( true );
283         d->m_routeOffsetLabel->setEnabled( false );
284         d->m_routeOffsetSpinBox->setEnabled( false );
285         d->m_latLonBoxWidget->setEnabled( false );
286         setSpecifiedLatLonAltBox( d->m_visibleRegion );
287         break;
288     case SpecifiedRegionMethod:
289         d->m_specifiedRegionMethodButton->setChecked( true );
290         d->m_routeOffsetLabel->setEnabled( false );
291         d->m_routeOffsetSpinBox->setEnabled( false );
292         d->m_latLonBoxWidget->setEnabled( true );
293         break;
294     case RouteDownloadMethod:
295         d->m_routeDownloadMethodButton->setChecked( true );
296         d->m_routeOffsetLabel->setEnabled( true );
297         d->m_routeOffsetSpinBox->setEnabled( true );
298         d->m_latLonBoxWidget->setEnabled( false );
299     }
300 
301     updateTilesCount();
302     d->m_visibleRegionMethodButton->blockSignals( false );
303     d->m_specifiedRegionMethodButton->blockSignals( false );
304     d->m_routeDownloadMethodButton->blockSignals( false );
305 }
306 
region() const307 QVector<TileCoordsPyramid> DownloadRegionDialog::region() const
308 {
309     if ( !d->hasTextureLayer() ) {
310         return QVector<TileCoordsPyramid>();
311     }
312 
313     d->m_downloadRegion.setTileLevelRange( d->m_tileLevelRangeWidget->topLevel(),
314                                            d->m_tileLevelRangeWidget->bottomLevel() );
315     d->m_downloadRegion.setVisibleTileLevel( d->m_visibleTileLevel );
316     // check whether "visible region" or "lat/lon region" is selection method
317     GeoDataLatLonAltBox downloadRegion;
318     switch ( d->m_selectionMethod ) {
319     case VisibleRegionMethod:
320         downloadRegion = d->m_visibleRegion;
321         break;
322     case SpecifiedRegionMethod:
323         downloadRegion = GeoDataLatLonAltBox( d->m_latLonBoxWidget->latLonBox(), 0, 0 );
324         break;
325    case RouteDownloadMethod:
326         qreal offset = d->m_routeOffsetSpinBox->value();
327         if (d->m_routeOffsetSpinBox->suffix() == QLatin1String(" km")) {
328             offset *= KM2METER;
329         }
330         const GeoDataLineString waypoints = d->m_model->routingManager()->routingModel()->route().path();
331         return d->m_downloadRegion.fromPath( d->m_textureLayer, offset, waypoints );
332     }
333 
334     return d->m_downloadRegion.region( d->m_textureLayer, downloadRegion );
335 }
336 
setSpecifiedLatLonAltBox(GeoDataLatLonAltBox const & region)337 void DownloadRegionDialog::setSpecifiedLatLonAltBox( GeoDataLatLonAltBox const & region )
338 {
339     d->m_latLonBoxWidget->setLatLonBox( region );
340 }
341 
setVisibleLatLonAltBox(GeoDataLatLonAltBox const & region)342 void DownloadRegionDialog::setVisibleLatLonAltBox( GeoDataLatLonAltBox const & region )
343 {
344     d->m_visibleRegion = region;
345 
346     // update lat/lon widget only if not active to prevent that users unintentionally loose
347     // entered values
348     if ( d->m_selectionMethod == VisibleRegionMethod ) {
349         setSpecifiedLatLonAltBox( region );
350     }
351     updateTilesCount();
352 }
353 
updateTextureLayer()354 void DownloadRegionDialog::updateTextureLayer()
355 {
356     mDebug() << "DownloadRegionDialog::updateTextureLayer";
357     updateTilesCount();
358 }
359 
hideEvent(QHideEvent * event)360 void DownloadRegionDialog::hideEvent( QHideEvent * event )
361 {
362     disconnect( d->m_widget, SIGNAL(visibleLatLonAltBoxChanged(GeoDataLatLonAltBox)),
363                 this, SLOT(setVisibleLatLonAltBox(GeoDataLatLonAltBox)) );
364     disconnect( d->m_widget, SIGNAL(themeChanged(QString)),
365                 this, SLOT(updateTextureLayer()) );
366 
367     emit hidden();
368     event->accept();
369 }
370 
showEvent(QShowEvent * event)371 void DownloadRegionDialog::showEvent( QShowEvent * event )
372 {
373     connect( d->m_widget, SIGNAL(visibleLatLonAltBoxChanged(GeoDataLatLonAltBox)),
374              this, SLOT(setVisibleLatLonAltBox(GeoDataLatLonAltBox)) );
375     connect( d->m_widget, SIGNAL(themeChanged(QString)),
376              this, SLOT(updateTextureLayer()) );
377 
378     emit shown();
379     event->accept();
380 }
381 
toggleSelectionMethod()382 void DownloadRegionDialog::toggleSelectionMethod()
383 {
384     // TODO:QButtonGroup would be easier to handle
385     switch ( d->m_selectionMethod ) {
386     case VisibleRegionMethod:
387         if( d->m_specifiedRegionMethodButton->isChecked() ) {
388             setSelectionMethod( SpecifiedRegionMethod );
389         }
390         else if( d->m_routeDownloadMethodButton->isChecked() ) {
391             setSelectionMethod( RouteDownloadMethod );
392         }
393 
394         break;
395     case SpecifiedRegionMethod:
396         if( d->m_visibleRegionMethodButton->isChecked() ) {
397             setSelectionMethod( VisibleRegionMethod );
398         }
399         else if ( d->m_routeDownloadMethodButton->isChecked() ) {
400             setSelectionMethod( RouteDownloadMethod );
401         }
402         break;
403     case RouteDownloadMethod:
404         if( d->m_specifiedRegionMethodButton->isChecked() ) {
405             setSelectionMethod( SpecifiedRegionMethod );
406         }
407         else if ( d->m_visibleRegionMethodButton->isChecked() ) {
408             setSelectionMethod( VisibleRegionMethod );
409         }
410         break;
411 
412     }
413 }
414 
updateTilesCount()415 void DownloadRegionDialog::updateTilesCount()
416 {
417     if ( !isVisible() || !d->hasTextureLayer() ) {
418         return;
419     }
420 
421     qint64 tilesCount = 0;
422     QString themeId( d->m_model->mapThemeId() );
423     QVector<TileCoordsPyramid> const pyramid = region();
424     Q_ASSERT( !pyramid.isEmpty() );
425     if( pyramid.size() == 1 ) {
426         tilesCount = pyramid[0].tilesCount();
427     }
428     else {
429         for( int level = pyramid[0].bottomLevel(); level>= pyramid[0].topLevel(); --level ) {
430             QSet<TileId> tileIdSet;
431             for( int i = 0; i < pyramid.size(); ++i ) {
432                 QRect const coords = pyramid[i].coords( level );
433                 int x1, y1, x2, y2;
434                 coords.getCoords( &x1, &y1, &x2, &y2 );
435                 for ( int x = x1; x <= x2; ++x ) {
436                     for ( int y = y1; y <= y2; ++y ) {
437                         TileId const tileId( 0, level, x, y );
438                         tileIdSet.insert( tileId );
439                     }
440                 }
441             }
442             tilesCount += tileIdSet.count();
443         }
444     }
445 
446     if ( tilesCount > maxTilesCount ) {
447         d->m_tileSizeInfo->setToolTip( QString() );
448         //~ singular There is a limit of %n tile to download.
449         //~ plural There is a limit of %n tiles to download.
450         d->m_tileSizeInfo->setText( tr( "There is a limit of %n tile(s) to download.", "",
451                                                maxTilesCount ) );
452     } else if (themeId == QLatin1String("earth/openstreetmap/openstreetmap.dgml")) {
453         qreal tileDownloadSize = tilesCount * averageTileSize;
454 
455         d->m_tileSizeInfo->setToolTip( tr( "Approximate size of the tiles to be downloaded" ) );
456 
457         if( tileDownloadSize > 1024 ) {
458             tileDownloadSize = tileDownloadSize / 1024;
459             d->m_tileSizeInfo->setText( tr( "Estimated download size: %1 MB" ).arg( ceil( tileDownloadSize ) ) );
460         }
461         else {
462             d->m_tileSizeInfo->setText( tr( "Estimated download size: %1 kB" ).arg( tileDownloadSize ) );
463         }
464 
465     }
466     else {
467         d->m_tileSizeInfo->setToolTip( QString() );
468         d->m_tileSizeInfo->clear();
469     }
470 
471     d->m_tilesCountLabel->setText( QString::number( tilesCount ) );
472     bool const tilesCountWithinLimits = tilesCount > 0 && tilesCount <= maxTilesCount;
473     d->m_okButton->setEnabled( tilesCountWithinLimits );
474     d->m_applyButton->setEnabled( tilesCountWithinLimits );
475 }
476 
updateRouteDialog()477 void DownloadRegionDialog::updateRouteDialog()
478 {
479     d->m_routeDownloadMethodButton->setEnabled( d->hasRoute() );
480     d->m_routeDownloadMethodButton->setChecked( d->hasRoute() );
481     if( !d->hasRoute() ) {
482         setSelectionMethod( VisibleRegionMethod );
483     }
484 }
485 
setOffsetUnit()486 void DownloadRegionDialog::setOffsetUnit()
487 {
488     qreal offset = d->m_routeOffsetSpinBox->value();
489 
490     if( offset >= 1100 ) {
491         d->m_routeOffsetSpinBox->setSuffix( " km" );
492         d->m_routeOffsetSpinBox->setRange( minimumRouteOffset * METER2KM, maximumRouteOffset * METER2KM );
493         d->m_routeOffsetSpinBox->setDecimals( 1 );
494         d->m_routeOffsetSpinBox->setValue( offset * METER2KM );
495         d->m_routeOffsetSpinBox->setSingleStep( 0.1 );
496     }
497     else if (offset <= 1 && d->m_routeOffsetSpinBox->suffix() == QLatin1String(" km")) {
498         d->m_routeOffsetSpinBox->setSuffix( " m" );
499         d->m_routeOffsetSpinBox->setRange( minimumRouteOffset, maximumRouteOffset );
500         d->m_routeOffsetSpinBox->setDecimals( 0 );
501         d->m_routeOffsetSpinBox->setValue( offset * KM2METER );
502         d->m_routeOffsetSpinBox->setSingleStep( 100 );
503     }
504 }
505 
506 }
507 
508 #include "moc_DownloadRegionDialog.cpp"
509