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