1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2008 Torsten Rahn <tackat@kde.org>
4 // SPDX-FileCopyrightText: 2012 Illya Kovalevskyy <illya.kovalevskyy@gmail.com>
5 //
6 
7 #include "MapScaleFloatItem.h"
8 
9 #include <QContextMenuEvent>
10 #include <QDebug>
11 #include <QHelpEvent>
12 #include <QRect>
13 #include <QPainter>
14 #include <QPushButton>
15 #include <QMenu>
16 #include <QToolTip>
17 
18 #include "ui_MapScaleConfigWidget.h"
19 #include "MarbleColors.h"
20 #include "MarbleDebug.h"
21 #include "MarbleGlobal.h"
22 #include "projections/AbstractProjection.h"
23 #include "MarbleLocale.h"
24 #include "MarbleModel.h"
25 #include "ViewportParams.h"
26 #include "GeoDataLatLonAltBox.h"
27 
28 namespace Marble
29 {
30 
MapScaleFloatItem(const MarbleModel * marbleModel)31 MapScaleFloatItem::MapScaleFloatItem( const MarbleModel *marbleModel )
32     : AbstractFloatItem( marbleModel, QPointF( 10.5, -10.5 ), QSizeF( 0.0, 40.0 ) ),
33       m_configDialog(nullptr),
34       m_radius(0),
35       m_target(QString()),
36       m_leftBarMargin(0),
37       m_rightBarMargin(0),
38       m_scaleBarWidth(0),
39       m_viewportWidth(0),
40       m_scaleBarHeight(5),
41       m_scaleBarDistance(0.0),
42       m_bestDivisor(0),
43       m_pixelInterval(0),
44       m_valueInterval(0),
45       m_scaleInitDone( false ),
46       m_showRatioScale( false ),
47       m_contextMenu( nullptr ),
48       m_minimized(false),
49       m_widthScaleFactor(2)
50 {
51     m_minimizeAction = new QAction(tr("Minimize"), this);
52     m_minimizeAction->setCheckable(true);
53     m_minimizeAction->setChecked(m_minimized);
54     connect(m_minimizeAction, SIGNAL(triggered()), this, SLOT(toggleMinimized()));
55 }
56 
~MapScaleFloatItem()57 MapScaleFloatItem::~MapScaleFloatItem()
58 {
59 }
60 
backendTypes() const61 QStringList MapScaleFloatItem::backendTypes() const
62 {
63     return QStringList(QStringLiteral("mapscale"));
64 }
65 
name() const66 QString MapScaleFloatItem::name() const
67 {
68     return tr("Scale Bar");
69 }
70 
guiString() const71 QString MapScaleFloatItem::guiString() const
72 {
73     return tr("&Scale Bar");
74 }
75 
nameId() const76 QString MapScaleFloatItem::nameId() const
77 {
78     return QStringLiteral("scalebar");
79 }
80 
version() const81 QString MapScaleFloatItem::version() const
82 {
83     return QStringLiteral("1.1");
84 }
85 
description() const86 QString MapScaleFloatItem::description() const
87 {
88     return tr("This is a float item that provides a map scale.");
89 }
90 
copyrightYears() const91 QString MapScaleFloatItem::copyrightYears() const
92 {
93     return QStringLiteral("2008, 2010, 2012");
94 }
95 
pluginAuthors() const96 QVector<PluginAuthor> MapScaleFloatItem::pluginAuthors() const
97 {
98     return QVector<PluginAuthor>()
99             << PluginAuthor(QStringLiteral("Torsten Rahn"), QStringLiteral("tackat@kde.org"), tr("Original Developer"))
100             << PluginAuthor(QStringLiteral("Khanh-Nhan Nguyen"), QStringLiteral("khanh.nhan@wpi.edu"))
101             << PluginAuthor(QStringLiteral("Illya Kovalevskyy"), QStringLiteral("illya.kovalevskyy@gmail.com"));
102 }
103 
icon() const104 QIcon MapScaleFloatItem::icon () const
105 {
106     return QIcon(QStringLiteral(":/icons/scalebar.png"));
107 }
108 
109 
initialize()110 void MapScaleFloatItem::initialize ()
111 {
112 }
113 
isInitialized() const114 bool MapScaleFloatItem::isInitialized () const
115 {
116     return true;
117 }
118 
setProjection(const ViewportParams * viewport)119 void MapScaleFloatItem::setProjection( const ViewportParams *viewport )
120 {
121     int viewportWidth = viewport->width();
122 
123     QString target = marbleModel()->planetId();
124 
125     if ( !(    m_radius == viewport->radius()
126             && viewportWidth == m_viewportWidth
127             && m_target == target
128             && m_scaleInitDone ) )
129     {
130         int fontHeight     = QFontMetrics( font() ).ascent();
131         if (m_showRatioScale) {
132             setContentSize( QSizeF( viewport->width() / m_widthScaleFactor,
133                                     fontHeight + 3 + m_scaleBarHeight + fontHeight + 7 ) );
134         } else {
135             setContentSize( QSizeF( viewport->width() / m_widthScaleFactor,
136                                     fontHeight + 3 + m_scaleBarHeight ) );
137         }
138 
139         m_leftBarMargin  = QFontMetrics( font() ).boundingRect( "0" ).width() / 2;
140         m_rightBarMargin = QFontMetrics( font() ).boundingRect( "0000" ).width() / 2;
141 
142         m_scaleBarWidth = contentSize().width() - m_leftBarMargin - m_rightBarMargin;
143         m_viewportWidth = viewport->width();
144         m_radius = viewport->radius();
145         m_scaleInitDone = true;
146 
147         m_pixel2Length = marbleModel()->planetRadius() /
148                              (qreal)(viewport->radius());
149 
150         if ( viewport->currentProjection()->surfaceType() == AbstractProjection::Cylindrical )
151         {
152             qreal centerLatitude = viewport->viewLatLonAltBox().center().latitude();
153             // For flat maps we calculate the length of the 90 deg section of the
154             // central latitude circle. For flat maps this distance matches
155             // the pixel based radius propertyy.
156             m_pixel2Length *= M_PI / 2 * cos( centerLatitude );
157         }
158 
159         m_scaleBarDistance = (qreal)(m_scaleBarWidth) * m_pixel2Length;
160 
161         const MarbleLocale::MeasurementSystem measurementSystem =
162                 MarbleGlobal::getInstance()->locale()->measurementSystem();
163 
164         if ( measurementSystem != MarbleLocale::MetricSystem ) {
165             m_scaleBarDistance *= KM2MI;
166         } else if (measurementSystem == MarbleLocale::NauticalSystem) {
167             m_scaleBarDistance *= KM2NM;
168         }
169 
170         calcScaleBar();
171 
172         update();
173     }
174 
175     AbstractFloatItem::setProjection( viewport );
176 }
177 
paintContent(QPainter * painter)178 void MapScaleFloatItem::paintContent( QPainter *painter )
179 {
180     painter->save();
181 
182     painter->setRenderHint( QPainter::Antialiasing, true );
183 
184     int fontHeight     = QFontMetrics( font() ).ascent();
185 
186     //calculate scale ratio
187     qreal displayMMPerPixel = 1.0 * painter->device()->widthMM() / painter->device()->width();
188     qreal ratio = m_pixel2Length / (displayMMPerPixel * MM2M);
189 
190     //round ratio to 3 most significant digits, assume that ratio >= 1, otherwise it may display "1 : 0"
191     //i made this assumption because as the primary use case we do not need to zoom in that much
192     qreal power = 1;
193     int iRatio = (int)(ratio + 0.5); //round ratio to the nearest integer
194     while (iRatio >= 1000) {
195         iRatio /= 10;
196         power *= 10;
197     }
198     iRatio *= power;
199     m_ratioString.setNum(iRatio);
200     m_ratioString = QLatin1String("1 : ") + m_ratioString;
201 
202     painter->setPen(   QColor( Qt::darkGray ) );
203     painter->setBrush( QColor( Qt::darkGray ) );
204     painter->drawRect( m_leftBarMargin, fontHeight + 3,
205                        m_scaleBarWidth,
206                        m_scaleBarHeight );
207 
208     painter->setPen(   QColor( Qt::black ) );
209     painter->setBrush( QColor( Qt::white ) );
210     painter->drawRect( m_leftBarMargin, fontHeight + 3,
211                        m_bestDivisor * m_pixelInterval, m_scaleBarHeight );
212 
213     painter->setPen(   QColor( Oxygen::aluminumGray4 ) );
214     painter->drawLine( m_leftBarMargin + 1, fontHeight + 2 + m_scaleBarHeight,
215                        m_leftBarMargin + m_bestDivisor * m_pixelInterval - 1, fontHeight + 2 + m_scaleBarHeight );
216     painter->setPen(   QColor( Qt::black ) );
217 
218     painter->setBrush( QColor( Qt::black ) );
219 
220     QString  intervalStr;
221     int      lastStringEnds     = 0;
222     int      currentStringBegin = 0;
223 
224     for ( int j = 0; j <= m_bestDivisor; j += 2 ) {
225         if ( j < m_bestDivisor ) {
226             painter->drawRect( m_leftBarMargin + j * m_pixelInterval,
227                                fontHeight + 3, m_pixelInterval - 1,
228                                m_scaleBarHeight );
229 
230 	    painter->setPen(   QColor( Oxygen::aluminumGray5 ) );
231 	    painter->drawLine( m_leftBarMargin + j * m_pixelInterval + 1, fontHeight + 4,
232 			       m_leftBarMargin + (j + 1) * m_pixelInterval - 1, fontHeight + 4 );
233 	    painter->setPen(   QColor( Qt::black ) );
234         }
235 
236         MarbleLocale::MeasurementSystem distanceUnit;
237         distanceUnit = MarbleGlobal::getInstance()->locale()->measurementSystem();
238 
239         QString unit = tr("km");
240         switch ( distanceUnit ) {
241         case MarbleLocale::MetricSystem:
242             if ( m_bestDivisor * m_valueInterval > 10000 ) {
243                 unit = tr("km");
244                 intervalStr.setNum( j * m_valueInterval / 1000 );
245             }
246             else {
247                 unit = tr("m");
248                 intervalStr.setNum( j * m_valueInterval );
249             }
250             break;
251         case MarbleLocale::ImperialSystem:
252             unit = tr("mi");
253             intervalStr.setNum( j * m_valueInterval / 1000 );
254 
255             if ( m_bestDivisor * m_valueInterval > 3800 ) {
256                 intervalStr.setNum( j * m_valueInterval / 1000 );
257             }
258             else {
259                 intervalStr.setNum( qreal(j * m_valueInterval ) / 1000.0, 'f', 2 );
260             }
261             break;
262         case MarbleLocale::NauticalSystem:
263             unit = tr("nm");
264             intervalStr.setNum( j * m_valueInterval / 1000 );
265 
266             if ( m_bestDivisor * m_valueInterval > 3800 ) {
267                 intervalStr.setNum( j * m_valueInterval / 1000 );
268             }
269             else {
270                 intervalStr.setNum( qreal(j * m_valueInterval ) / 1000.0, 'f', 2 );
271             }
272             break;
273         }
274 
275         painter->setFont( font() );
276 
277         if ( j == 0 ) {
278             const QString text = QLatin1String("0 ") + unit;
279             painter->drawText(0, fontHeight, text);
280             lastStringEnds = QFontMetrics(font()).width(text);
281             continue;
282         }
283 
284         if( j == m_bestDivisor ) {
285             currentStringBegin = ( j * m_pixelInterval
286                                    - QFontMetrics( font() ).boundingRect( intervalStr ).width() );
287         }
288         else {
289             currentStringBegin = ( j * m_pixelInterval
290                                    - QFontMetrics( font() ).width( intervalStr ) / 2 );
291         }
292 
293         if ( lastStringEnds < currentStringBegin ) {
294             painter->drawText( currentStringBegin, fontHeight, intervalStr );
295             lastStringEnds = currentStringBegin + QFontMetrics( font() ).width( intervalStr );
296         }
297     }
298 
299     int leftRatioIndent = m_leftBarMargin + (m_scaleBarWidth - QFontMetrics( font() ).width(m_ratioString) ) / 2;
300     painter->drawText( leftRatioIndent, fontHeight + 3 + m_scaleBarHeight + fontHeight + 5, m_ratioString );
301 
302     painter->restore();
303 }
304 
calcScaleBar()305 void MapScaleFloatItem::calcScaleBar()
306 {
307     qreal  magnitude = 1;
308 
309     // First we calculate the exact length of the whole area that is possibly
310     // available to the scalebar in kilometers
311     int  magValue = (int)( m_scaleBarDistance );
312 
313     // We calculate the two most significant digits of the km-scalebar-length
314     // and store them in magValue.
315     while ( magValue >= 100 ) {
316         magValue  /= 10;
317         magnitude *= 10;
318     }
319 
320     m_bestDivisor = 4;
321     int  bestMagValue = 1;
322 
323     for ( int i = 0; i < magValue; i++ ) {
324         // We try to find the lowest divisor between 4 and 8 that
325         // divides magValue without remainder.
326         for ( int j = 4; j < 9; j++ ) {
327             if ( ( magValue - i ) % j == 0 ) {
328                 // We store the very first result we find and store
329                 // m_bestDivisor and bestMagValue as a final result.
330                 m_bestDivisor = j;
331                 bestMagValue  = magValue - i;
332 
333                 // Stop all for loops and end search
334                 i = magValue;
335                 j = 9;
336             }
337         }
338 
339         // If magValue doesn't divide through values between 4 and 8
340         // (e.g. because it's a prime number) try again with magValue
341         // decreased by i.
342     }
343 
344     m_pixelInterval = (int)( m_scaleBarWidth * (qreal)( bestMagValue )
345                              / (qreal)( magValue ) / m_bestDivisor );
346     m_valueInterval = (int)( bestMagValue * magnitude / m_bestDivisor );
347 }
348 
configDialog()349 QDialog *MapScaleFloatItem::configDialog()
350 {
351     if ( !m_configDialog ) {
352         // Initializing configuration dialog
353         m_configDialog = new QDialog();
354         ui_configWidget = new Ui::MapScaleConfigWidget;
355         ui_configWidget->setupUi( m_configDialog );
356 
357         readSettings();
358 
359         connect( ui_configWidget->m_buttonBox, SIGNAL(accepted()),
360                                             SLOT(writeSettings()) );
361         connect( ui_configWidget->m_buttonBox, SIGNAL(rejected()),
362                                             SLOT(readSettings()) );
363 
364         QPushButton *applyButton = ui_configWidget->m_buttonBox->button( QDialogButtonBox::Apply );
365         connect( applyButton, SIGNAL(clicked()) ,
366                 this,        SLOT(writeSettings()) );
367     }
368     return m_configDialog;
369 }
370 
contextMenuEvent(QWidget * w,QContextMenuEvent * e)371 void MapScaleFloatItem::contextMenuEvent( QWidget *w, QContextMenuEvent *e )
372 {
373     if ( !m_contextMenu ) {
374         m_contextMenu = contextMenu();
375 
376         for( QAction *action: m_contextMenu->actions() ) {
377             if ( action->text() == tr( "&Configure..." ) ) {
378                 m_contextMenu->removeAction( action );
379                 break;
380             }
381         }
382 
383         QAction *toggleAction = m_contextMenu->addAction( tr("&Ratio Scale"), this,
384                                                 SLOT(toggleRatioScaleVisibility()) );
385         toggleAction->setCheckable( true );
386         toggleAction->setChecked( m_showRatioScale );
387 
388         m_contextMenu->addAction(m_minimizeAction);
389     }
390 
391     Q_ASSERT( m_contextMenu );
392     m_contextMenu->exec( w->mapToGlobal( e->pos() ) );
393 }
394 
toolTipEvent(QHelpEvent * e)395 void MapScaleFloatItem::toolTipEvent( QHelpEvent *e )
396 {
397     QToolTip::showText( e->globalPos(), m_ratioString );
398 }
399 
readSettings()400 void MapScaleFloatItem::readSettings()
401 {
402     if ( !m_configDialog )
403         return;
404 
405     if ( m_showRatioScale ) {
406         ui_configWidget->m_showRatioScaleCheckBox->setCheckState( Qt::Checked );
407     } else {
408         ui_configWidget->m_showRatioScaleCheckBox->setCheckState( Qt::Unchecked );
409     }
410 
411     ui_configWidget->m_minimizeCheckBox->setChecked(m_minimized);
412 }
413 
writeSettings()414 void MapScaleFloatItem::writeSettings()
415 {
416     if ( ui_configWidget->m_showRatioScaleCheckBox->checkState() == Qt::Checked ) {
417         m_showRatioScale = true;
418     } else {
419         m_showRatioScale = false;
420     }
421 
422     if (m_minimized != ui_configWidget->m_minimizeCheckBox->isChecked()) {
423         toggleMinimized();
424     }
425 
426     emit settingsChanged( nameId() );
427 }
428 
toggleRatioScaleVisibility()429 void MapScaleFloatItem::toggleRatioScaleVisibility()
430 {
431     m_showRatioScale = !m_showRatioScale;
432     readSettings();
433     emit settingsChanged( nameId() );
434 }
435 
toggleMinimized()436 void MapScaleFloatItem::toggleMinimized()
437 {
438     m_minimized = !m_minimized;
439     ui_configWidget->m_minimizeCheckBox->setChecked(m_minimized);
440     m_minimizeAction->setChecked(m_minimized);
441     readSettings();
442     emit settingsChanged( nameId() );
443 
444     if (m_minimized == true) {
445         m_widthScaleFactor = 4;
446     } else {
447         m_widthScaleFactor = 2;
448     }
449 }
450 
451 }
452 
453 #include "moc_MapScaleFloatItem.cpp"
454