1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2008 Torsten Rahn <tackat@kde.org>
4 //
5 
6 #include "OverviewMap.h"
7 
8 #include <QRect>
9 #include <QStringList>
10 #include <QCursor>
11 #include <QMouseEvent>
12 #include <QPainter>
13 #include <QFileDialog>
14 #include <QHBoxLayout>
15 #include <QColorDialog>
16 
17 #include "MarbleDirs.h"
18 #include "MarbleDebug.h"
19 #include "MarbleModel.h"
20 #include "ui_OverviewMapConfigWidget.h"
21 
22 #include "GeoDataPoint.h"
23 #include "ViewportParams.h"
24 #include "MarbleWidget.h"
25 #include "Planet.h"
26 #include "PlanetFactory.h"
27 
28 namespace Marble
29 {
30 
OverviewMap()31 OverviewMap::OverviewMap()
32     : AbstractFloatItem( nullptr ),
33       ui_configWidget( nullptr ),
34       m_configDialog( nullptr ),
35       m_mapChanged( false )
36 {
37 }
38 
OverviewMap(const MarbleModel * marbleModel)39 OverviewMap::OverviewMap( const MarbleModel *marbleModel )
40     : AbstractFloatItem( marbleModel, QPointF( 10.5, 10.5 ), QSizeF( 166.0, 86.0 ) ),
41       m_target(),
42       m_planetID( PlanetFactory::planetList() ),
43       m_defaultSize( AbstractFloatItem::size() ),
44       ui_configWidget( nullptr ),
45       m_configDialog( nullptr ),
46       m_mapChanged( false )
47 {
48     // cache is no needed because:
49     // (1) the SVG overview map is already rendered and stored in m_worldmap pixmap
50     // (2) bounding box and location dot keep changing during navigation
51     setCacheMode( NoCache );
52     connect( this, SIGNAL(settingsChanged(QString)),
53              this, SLOT(updateSettings()) );
54 
55     restoreDefaultSettings();
56 }
57 
~OverviewMap()58 OverviewMap::~OverviewMap()
59 {
60     QHash<QString, QSvgWidget *>::const_iterator pos = m_svgWidgets.constBegin();
61     QHash<QString, QSvgWidget *>::const_iterator const end = m_svgWidgets.constEnd();
62     for (; pos != end; ++pos ) {
63         delete pos.value();
64     }
65 }
66 
backendTypes() const67 QStringList OverviewMap::backendTypes() const
68 {
69     return QStringList(QStringLiteral("overviewmap"));
70 }
71 
name() const72 QString OverviewMap::name() const
73 {
74     return tr("Overview Map");
75 }
76 
guiString() const77 QString OverviewMap::guiString() const
78 {
79     return tr("&Overview Map");
80 }
81 
nameId() const82 QString OverviewMap::nameId() const
83 {
84     return QStringLiteral("overviewmap");
85 }
86 
version() const87 QString OverviewMap::version() const
88 {
89     return QStringLiteral("1.0");
90 }
91 
description() const92 QString OverviewMap::description() const
93 {
94     return tr("This is a float item that provides an overview map.");
95 }
96 
copyrightYears() const97 QString OverviewMap::copyrightYears() const
98 {
99     return QStringLiteral("2008");
100 }
101 
pluginAuthors() const102 QVector<PluginAuthor> OverviewMap::pluginAuthors() const
103 {
104     return QVector<PluginAuthor>()
105             << PluginAuthor(QStringLiteral("Torsten Rahn"), QStringLiteral("tackat@kde.org"));
106 }
107 
icon() const108 QIcon OverviewMap::icon () const
109 {
110     return QIcon(QStringLiteral(":/icons/worldmap.png"));
111 }
112 
configDialog()113 QDialog *OverviewMap::configDialog()
114 {
115     if ( !m_configDialog ) {
116         // Initializing configuration dialog
117         m_configDialog = new QDialog();
118         ui_configWidget = new Ui::OverviewMapConfigWidget;
119         ui_configWidget->setupUi( m_configDialog );
120         for( int i = 0; i < m_planetID.size(); ++i ) {
121             ui_configWidget->m_planetComboBox->addItem( PlanetFactory::localizedName(m_planetID.value( i ) ) );
122         }
123         ui_configWidget->m_planetComboBox->setCurrentIndex( 2 );
124         readSettings();
125         loadMapSuggestions();
126         connect( ui_configWidget->m_buttonBox, SIGNAL(accepted()),
127                  SLOT(writeSettings()) );
128         connect( ui_configWidget->m_buttonBox, SIGNAL(rejected()),
129                  SLOT(readSettings()) );
130         connect( ui_configWidget->m_buttonBox->button( QDialogButtonBox::Reset ), SIGNAL(clicked()),
131                  SLOT(restoreDefaultSettings()) );
132         QPushButton *applyButton = ui_configWidget->m_buttonBox->button( QDialogButtonBox::Apply );
133         connect( applyButton, SIGNAL(clicked()),
134                  SLOT(writeSettings()) );
135         connect( ui_configWidget->m_fileChooserButton, SIGNAL(clicked()),
136                  SLOT(chooseCustomMap()) );
137         connect( ui_configWidget->m_widthBox, SIGNAL(valueChanged(int)),
138                  SLOT(synchronizeSpinboxes()) );
139         connect( ui_configWidget->m_heightBox, SIGNAL(valueChanged(int)),
140                  SLOT(synchronizeSpinboxes()) );
141         connect( ui_configWidget->m_planetComboBox, SIGNAL(currentIndexChanged(int)),
142                  SLOT(showCurrentPlanetPreview()) );
143         connect( ui_configWidget->m_colorChooserButton, SIGNAL(clicked()),
144                  SLOT(choosePositionIndicatorColor()) );
145         connect( ui_configWidget->m_tableWidget, SIGNAL(cellClicked(int,int)),
146                  SLOT(useMapSuggestion(int)) );
147     }
148     return m_configDialog;
149 }
150 
initialize()151 void OverviewMap::initialize ()
152 {
153 }
154 
isInitialized() const155 bool OverviewMap::isInitialized () const
156 {
157     return true;
158 }
159 
setProjection(const ViewportParams * viewport)160 void OverviewMap::setProjection( const ViewportParams *viewport )
161 {
162     GeoDataLatLonAltBox latLonAltBox = viewport->latLonAltBox( QRect( QPoint( 0, 0 ), viewport->size() ) );
163     const qreal centerLon = viewport->centerLongitude();
164     const qreal centerLat = viewport->centerLatitude();
165     QString target = marbleModel()->planetId();
166 
167     if ( target != m_target ) {
168         changeBackground( target );
169         m_target = target;
170         update();
171     }
172 
173     if ( !( m_latLonAltBox == latLonAltBox
174             && m_centerLon == centerLon
175             && m_centerLat == centerLat ) )
176     {
177         m_latLonAltBox = latLonAltBox;
178         m_centerLon = centerLon;
179         m_centerLat = centerLat;
180         update();
181     }
182 
183     AbstractFloatItem::setProjection( viewport );
184 }
185 
paintContent(QPainter * painter)186 void OverviewMap::paintContent( QPainter *painter )
187 {
188     painter->save();
189 
190     QRectF mapRect( contentRect() );
191 
192     if ( m_svgobj.isValid() ) {
193         // Rerender worldmap pixmap if the size or map has changed
194         if ( m_worldmap.size() != mapRect.size().toSize() || m_mapChanged ) {
195             m_mapChanged = false;
196             m_worldmap = QPixmap( mapRect.size().toSize() );
197             m_worldmap.fill( Qt::transparent );
198             QPainter mapPainter;
199             mapPainter.begin( &m_worldmap );
200             mapPainter.setViewport( m_worldmap.rect() );
201             m_svgobj.render( &mapPainter );
202             mapPainter.end();
203         }
204 
205         painter->drawPixmap( QPoint( 0, 0 ), m_worldmap );
206     }
207     else {
208         painter->setPen( QPen( Qt::DashLine ) );
209         painter->drawRect( QRectF( QPoint( 0, 0 ), mapRect.size().toSize() ) );
210 
211         for ( int y = 1; y < 4; ++y ) {
212             if ( y == 2 ) {
213                 painter->setPen( QPen( Qt::DashLine ) );
214             }
215             else {
216                 painter->setPen( QPen( Qt::DotLine ) );
217             }
218 
219             painter->drawLine( 0.0, 0.25 * y * mapRect.height(),
220                                 mapRect.width(), 0.25 * y * mapRect.height() );
221         }
222         for ( int x = 1; x < 8; ++x ) {
223             if ( x == 4 ) {
224                 painter->setPen( QPen( Qt::DashLine ) );
225             }
226             else {
227                 painter->setPen( QPen( Qt::DotLine ) );
228             }
229 
230             painter->drawLine( 0.125 * x * mapRect.width(), 0,
231                                0.125 * x * mapRect.width(), mapRect.height() );
232         }
233     }
234 
235     // Now draw the latitude longitude bounding box
236     qreal xWest = mapRect.width() / 2.0
237                     + mapRect.width() / ( 2.0 * M_PI ) * m_latLonAltBox.west();
238     qreal xEast = mapRect.width() / 2.0
239                     + mapRect.width() / ( 2.0 * M_PI ) * m_latLonAltBox.east();
240     qreal xNorth = mapRect.height() / 2.0
241                     - mapRect.height() / M_PI * m_latLonAltBox.north();
242     qreal xSouth = mapRect.height() / 2.0
243                     - mapRect.height() / M_PI * m_latLonAltBox.south();
244 
245     qreal lon = m_centerLon;
246     qreal lat = m_centerLat;
247     GeoDataCoordinates::normalizeLonLat( lon, lat );
248     qreal x = mapRect.width() / 2.0 + mapRect.width() / ( 2.0 * M_PI ) * lon;
249     qreal y = mapRect.height() / 2.0 - mapRect.height() / M_PI * lat;
250 
251     painter->setPen( QPen( Qt::white ) );
252     painter->setBrush( QBrush( Qt::transparent ) );
253     painter->setRenderHint( QPainter::Antialiasing, false );
254 
255     qreal boxWidth  = xEast  - xWest;
256     qreal boxHeight = xSouth - xNorth;
257 
258     qreal minBoxSize = 2.0;
259     if ( boxHeight < minBoxSize ) boxHeight = minBoxSize;
260 
261     if ( m_latLonAltBox.west() <= m_latLonAltBox.east() ) {
262         // Make sure the latLonBox is still visible
263         if ( boxWidth  < minBoxSize ) boxWidth  = minBoxSize;
264 
265         painter->drawRect( QRectF( xWest, xNorth, boxWidth, boxHeight ) );
266     }
267     else {
268         // If the dateline is shown in the viewport  and if the poles are not
269         // then there are two boxes that represent the latLonBox of the view.
270 
271         boxWidth = xEast;
272 
273         // Make sure the latLonBox is still visible
274         if ( boxWidth  < minBoxSize ) boxWidth  = minBoxSize;
275 
276         painter->drawRect( QRectF( 0, xNorth, boxWidth, boxHeight ) );
277 
278         boxWidth = mapRect.width() - xWest;
279 
280         // Make sure the latLonBox is still visible
281         if ( boxWidth  < minBoxSize ) boxWidth  = minBoxSize;
282 
283         painter->drawRect( QRectF( xWest, xNorth, boxWidth, boxHeight ) );
284     }
285 
286     painter->setPen( QPen( m_posColor ) );
287     painter->setBrush( QBrush( m_posColor ) );
288 
289     qreal circleRadius = 2.5;
290     painter->setRenderHint( QPainter::Antialiasing, true );
291     painter->drawEllipse( QRectF( x - circleRadius, y - circleRadius , 2 * circleRadius, 2 * circleRadius ) );
292 
293     painter->restore();
294 }
295 
settings() const296 QHash<QString,QVariant> OverviewMap::settings() const
297 {
298     QHash<QString, QVariant> result = AbstractFloatItem::settings();
299 
300     typedef QHash<QString, QVariant>::ConstIterator Iterator;
301     Iterator end = m_settings.constEnd();
302     for ( Iterator iter = m_settings.constBegin(); iter != end; ++iter ) {
303         result.insert( iter.key(), iter.value() );
304     }
305 
306     return result;
307 }
308 
setSettings(const QHash<QString,QVariant> & settings)309 void OverviewMap::setSettings( const QHash<QString,QVariant> &settings )
310 {
311     AbstractFloatItem::setSettings( settings );
312 
313     m_settings.insert(QStringLiteral("width"), settings.value(QStringLiteral("width"), m_defaultSize.toSize().width()));
314     m_settings.insert(QStringLiteral("height"), settings.value(QStringLiteral("height"), m_defaultSize.toSize().height()));
315 
316     for ( const QString& planet: PlanetFactory::planetList() ) {
317         QString mapFile = MarbleDirs::path(QLatin1String("svg/") + planet + QLatin1String("map.svg"));
318 
319         if (planet == QLatin1String("moon")) {
320             mapFile = MarbleDirs::path(QStringLiteral("svg/lunarmap.svg"));
321         }
322         else if (planet == QLatin1String("earth") || mapFile.isEmpty()) {
323             mapFile = MarbleDirs::path(QStringLiteral("svg/worldmap.svg"));
324         }
325 
326         const QString id = QLatin1String("path_") + planet;
327         m_settings.insert(id, settings.value(id, mapFile));
328     }
329 
330     m_settings.insert(QStringLiteral("posColor"), settings.value(QStringLiteral("posColor"), QColor(Qt::white).name()));
331 
332     m_target.clear(); // FIXME: forces execution of changeBackground() in changeViewport()
333 
334     readSettings();
335     emit settingsChanged( nameId() );
336 }
337 
readSettings()338 void OverviewMap::readSettings()
339 {
340     if ( !m_configDialog ) {
341         return;
342     }
343 
344     ui_configWidget->m_widthBox->setValue( m_settings.value(QStringLiteral("width")).toInt() );
345     ui_configWidget->m_heightBox->setValue( m_settings.value(QStringLiteral("height")).toInt() );
346     QPalette palette = ui_configWidget->m_colorChooserButton->palette();
347     palette.setColor(QPalette::Button, QColor(m_settings.value(QStringLiteral("posColor")).toString()));
348     ui_configWidget->m_colorChooserButton->setPalette( palette );
349 }
350 
writeSettings()351 void OverviewMap::writeSettings()
352 {
353     if ( !m_configDialog ) {
354         return;
355     }
356 
357     m_settings.insert(QStringLiteral("width"), contentRect().width());
358     m_settings.insert(QStringLiteral("height"), contentRect().height());
359 
360     QStringList const planets = PlanetFactory::planetList();
361     for( const QString &planet: planets ) {
362         m_settings.insert(QLatin1String("path_") + planet, m_svgPaths[planet]);
363     }
364 
365     m_settings.insert(QStringLiteral("posColor"), m_posColor.name());
366 
367     emit settingsChanged( nameId() );
368 }
369 
updateSettings()370 void OverviewMap::updateSettings()
371 {
372     QStringList const planets = PlanetFactory::planetList();
373     for( const QString &planet: planets ) {
374         m_svgPaths.insert(planet, m_settings.value(QLatin1String("path_") + planet, QString()).toString());
375     }
376 
377     m_posColor = QColor(m_settings.value(QStringLiteral("posColor")).toString());
378     loadPlanetMaps();
379 
380     if ( !m_configDialog ) {
381         return;
382     }
383 
384     setCurrentWidget( m_svgWidgets[m_planetID[2]] );
385     showCurrentPlanetPreview();
386     setContentSize( QSizeF( ui_configWidget->m_widthBox->value(), ui_configWidget->m_heightBox->value() ) );
387 }
388 
eventFilter(QObject * object,QEvent * e)389 bool OverviewMap::eventFilter( QObject *object, QEvent *e )
390 {
391     if ( !enabled() || !visible() ) {
392         return false;
393     }
394 
395     MarbleWidget *widget = dynamic_cast<MarbleWidget*>(object);
396     if ( !widget ) {
397         return AbstractFloatItem::eventFilter(object,e);
398     }
399 
400     if ( e->type() == QEvent::MouseButtonDblClick || e->type() == QEvent::MouseMove ) {
401         QMouseEvent *event = static_cast<QMouseEvent*>(e);
402         QRectF floatItemRect = QRectF( positivePosition(), size() );
403 
404         bool cursorAboveFloatItem(false);
405         if ( floatItemRect.contains(event->pos()) ) {
406             cursorAboveFloatItem = true;
407 
408             // Double click triggers recentering the map at the specified position
409             if ( e->type() == QEvent::MouseButtonDblClick ) {
410                 QRectF mapRect( contentRect() );
411                 QPointF pos = event->pos() - floatItemRect.topLeft()
412                     - QPointF(padding(),padding());
413 
414                 qreal lon = ( pos.x() - mapRect.width() / 2.0 ) / mapRect.width() * 360.0 ;
415                 qreal lat = ( mapRect.height() / 2.0 - pos.y() ) / mapRect.height() * 180.0;
416                 widget->centerOn(lon,lat,true);
417 
418                 return true;
419             }
420         }
421 
422         if ( cursorAboveFloatItem && e->type() == QEvent::MouseMove
423                 && !(event->buttons() & Qt::LeftButton) )
424         {
425             // Cross hair cursor when moving above the float item without pressing a button
426             widget->setCursor(QCursor(Qt::CrossCursor));
427             return true;
428         }
429     }
430 
431     return AbstractFloatItem::eventFilter(object,e);
432 }
433 
changeBackground(const QString & target)434 void OverviewMap::changeBackground( const QString& target )
435 {
436     m_svgobj.load( m_svgPaths[target] );
437     m_mapChanged = true;
438 }
439 
currentWidget() const440 QSvgWidget *OverviewMap::currentWidget() const
441 {
442     return m_svgWidgets[m_planetID[ui_configWidget->m_planetComboBox->currentIndex()]];
443 }
444 
setCurrentWidget(QSvgWidget * widget)445 void OverviewMap::setCurrentWidget( QSvgWidget *widget )
446 {
447     m_svgWidgets[m_planetID[ui_configWidget->m_planetComboBox->currentIndex()]] = widget;
448     if( m_target == m_planetID[ui_configWidget->m_planetComboBox->currentIndex()] ) {
449         changeBackground( m_target );
450     }
451 }
452 
loadPlanetMaps()453 void OverviewMap::loadPlanetMaps()
454 {
455     for( const QString& planet: m_planetID ) {
456         if ( m_svgWidgets.contains( planet) ) {
457             m_svgWidgets[planet]->load( m_svgPaths[planet] );
458         } else {
459             m_svgWidgets[planet] = new QSvgWidget( m_svgPaths[planet] );
460         }
461     }
462 }
463 
loadMapSuggestions()464 void OverviewMap::loadMapSuggestions()
465 {
466     QStringList paths = QDir(MarbleDirs::pluginPath(QString())).entryList(QStringList("*.svg"), QDir::Files | QDir::NoDotAndDotDot);
467     for( int i = 0; i < paths.size(); ++i ) {
468         paths[i] = MarbleDirs::pluginPath(QString()) + QLatin1Char('/') + paths[i];
469     }
470     paths << MarbleDirs::path(QStringLiteral("svg/worldmap.svg")) << MarbleDirs::path(QStringLiteral("svg/lunarmap.svg"));
471     ui_configWidget->m_tableWidget->setRowCount( paths.size() );
472     for( int i = 0; i < paths.size(); ++i ) {
473         ui_configWidget->m_tableWidget->setCellWidget( i, 0, new QSvgWidget( paths[i] ) );
474         ui_configWidget->m_tableWidget->setItem( i, 1, new QTableWidgetItem( paths[i] ) );
475     }
476 }
477 
chooseCustomMap()478 void OverviewMap::chooseCustomMap()
479 {
480     QString path = QFileDialog::getOpenFileName ( m_configDialog, tr( "Choose Overview Map" ), "", "SVG (*.svg)" );
481     if( !path.isNull() )
482     {
483         ui_configWidget->m_fileChooserButton->layout()->removeWidget( currentWidget() );
484         delete currentWidget();
485         QSvgWidget *widget = new QSvgWidget( path );
486         setCurrentWidget( widget );
487         ui_configWidget->m_fileChooserButton->layout()->addWidget( widget );
488         m_svgPaths[m_planetID[ui_configWidget->m_planetComboBox->currentIndex()]] = path;
489     }
490 }
491 
synchronizeSpinboxes()492 void OverviewMap::synchronizeSpinboxes()
493 {
494     if( sender() == ui_configWidget->m_widthBox ) {
495         ui_configWidget->m_heightBox->setValue( ui_configWidget->m_widthBox->value() / 2 );
496     }
497     else if( sender() == ui_configWidget->m_heightBox ) {
498         ui_configWidget->m_widthBox->setValue( ui_configWidget->m_heightBox->value() * 2 );
499     }
500 }
501 
showCurrentPlanetPreview() const502 void OverviewMap::showCurrentPlanetPreview() const
503 {
504     delete ui_configWidget->m_fileChooserButton->layout();
505     ui_configWidget->m_fileChooserButton->setLayout( new QHBoxLayout() );
506     ui_configWidget->m_fileChooserButton->layout()->addWidget( currentWidget() );
507 }
508 
choosePositionIndicatorColor()509 void OverviewMap::choosePositionIndicatorColor()
510 {
511     QColor c = QColorDialog::getColor( m_posColor, nullptr,
512                                        tr( "Please choose the color for the position indicator" ),
513                                        QColorDialog::ShowAlphaChannel );
514     if( c.isValid() )
515     {
516         m_posColor = c;
517         QPalette palette = ui_configWidget->m_colorChooserButton->palette();
518         palette.setColor( QPalette::Button, m_posColor );
519         ui_configWidget->m_colorChooserButton->setPalette( palette );
520     }
521 }
522 
useMapSuggestion(int index)523 void OverviewMap::useMapSuggestion( int index )
524 {
525     QString path = ui_configWidget->m_tableWidget->item( index, 1 )->text();
526     m_svgPaths[m_planetID[ui_configWidget->m_planetComboBox->currentIndex()]] = path;
527     delete currentWidget();
528     QSvgWidget *widget = new QSvgWidget( path );
529     setCurrentWidget( widget );
530     showCurrentPlanetPreview();
531 }
532 
533 }
534 
535 #include "moc_OverviewMap.cpp"
536