1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2008 Henry de Valence <hdevalence@gmail.com>
4 // SPDX-FileCopyrightText: 2011 Friedrich W. H. Kossebau <kossebau@kde.org>
5 
6 
7 #include "LatLonEdit.h"
8 #include "ui_LatLonEdit.h"
9 
10 #include "MarbleDebug.h"
11 
12 
13 
14 namespace Marble
15 {
16 
17 // This widget can have 3 different designs, one per notation (Decimal, DMS, DM)
18 // To reduce the footprint this was not implemented using a stack of widgets
19 // where each widget offer the needed design for another notation.
20 // Instead, as Decimal and DM are both using subsets of the UI elements used
21 // for DMS, just the UI elements for DMS are created and modified as needed,
22 // if another notation is selected. This involves showing and hiding them and
23 // setting the proper suffix and min/max values.
24 // The logic per notation is moved into specialized subclasses of a class
25 // AbstractInputHandler.
26 // TODO: simply remove the LatLonEdit.ui file and embed code directly here?
27 
28 enum { PositiveSphereIndex = 0, NegativeSphereIndex = 1 };
29 
30 
31 class LatLonEditPrivate;
32 
33 class AbstractInputHandler // TODO: better name
34 {
35 protected:
AbstractInputHandler(LatLonEditPrivate * ui)36     explicit AbstractInputHandler(LatLonEditPrivate *ui) : m_ui(ui) {}
37 public:
~AbstractInputHandler()38     virtual ~AbstractInputHandler() {}
39 
40 public: // API to be implemented
41     virtual void setupUi() = 0;
42     virtual void setupMinMax(LatLonEdit::Dimension dimension) = 0;
43     virtual void setValue(qreal value) = 0;
44     virtual void handleIntEditChange() = 0;
45     virtual void handleUIntEditChange() = 0;
46     virtual void handleFloatEditChange() = 0;
47     virtual qreal calculateValue() const = 0;
48 
49 protected:
50     LatLonEditPrivate * const m_ui;
51 };
52 
53 class DecimalInputHandler : public AbstractInputHandler
54 {
55 public:
DecimalInputHandler(LatLonEditPrivate * ui)56     explicit DecimalInputHandler(LatLonEditPrivate *ui) : AbstractInputHandler(ui) {}
57 public: // AbstractInputHandler API
58     void setupUi() override;
59     void setupMinMax(LatLonEdit::Dimension dimension) override;
60     void setValue(qreal value) override;
61     void handleIntEditChange() override;
62     void handleUIntEditChange() override;
63     void handleFloatEditChange() override;
64     qreal calculateValue() const override;
65 };
66 
67 class DMSInputHandler : public AbstractInputHandler
68 {
69 public:
DMSInputHandler(LatLonEditPrivate * ui)70     explicit DMSInputHandler(LatLonEditPrivate *ui) : AbstractInputHandler(ui) {}
71 public: // AbstractInputHandler API
72     void setupUi() override;
73     void setupMinMax(LatLonEdit::Dimension dimension) override;
74     void setValue(qreal value) override;
75     void handleIntEditChange() override;
76     void handleUIntEditChange() override;
77     void handleFloatEditChange() override;
78     qreal calculateValue() const override;
79 };
80 
81 class DMInputHandler : public AbstractInputHandler
82 {
83 public:
DMInputHandler(LatLonEditPrivate * ui)84     explicit DMInputHandler(LatLonEditPrivate *ui) : AbstractInputHandler(ui) {}
85 public: // AbstractInputHandler API
86     void setupUi() override;
87     void setupMinMax(LatLonEdit::Dimension dimension) override;
88     void setValue(qreal value) override;
89     void handleIntEditChange() override;
90     void handleUIntEditChange() override;
91     void handleFloatEditChange() override;
92 
93     qreal calculateValue() const override;
94 };
95 
96 class LatLonEditPrivate : public Ui::LatLonEditPrivate
97 {
98     friend class DecimalInputHandler;
99     friend class DMSInputHandler;
100     friend class DMInputHandler;
101 
102 public:
103     LatLonEdit::Dimension m_dimension;
104     qreal m_value;
105     GeoDataCoordinates::Notation m_notation;
106     AbstractInputHandler *m_inputHandler;
107     // flag which indicates that the widgets are updated due to a change
108     // in one of the other widgets. Q*SpinBox does not have a signal which is
109     // only emitted by a change due to user input, not code setting a new value.
110     // This flag should be less expensive then disconnecting from and reconnecting
111     // to the valueChanged signal of all widgets.
112     bool m_updating : 1;
113 
114     LatLonEditPrivate();
115     ~LatLonEditPrivate();
116     void init(QWidget* parent);
117 };
118 
119 
120 static void
switchSign(QComboBox * sign)121 switchSign( QComboBox *sign )
122 {
123     const bool isNegativeSphere = (sign->currentIndex() == NegativeSphereIndex);
124     sign->setCurrentIndex( isNegativeSphere ? PositiveSphereIndex : NegativeSphereIndex );
125 }
126 
setupUi()127 void DecimalInputHandler::setupUi()
128 {
129     m_ui->m_floatValueEditor->setSuffix(LatLonEdit::trUtf8("\xC2\xB0")); // the degree symbol °
130     m_ui->m_floatValueEditor->setDecimals(5);
131 
132     m_ui->m_intValueEditor->hide();
133     m_ui->m_uintValueEditor->hide();
134 }
135 
setupMinMax(LatLonEdit::Dimension dimension)136 void DecimalInputHandler::setupMinMax(LatLonEdit::Dimension dimension)
137 {
138     const qreal maxValue = (dimension == LatLonEdit::Longitude) ? 180.0 : 90.0;
139 
140     m_ui->m_floatValueEditor->setMinimum(-maxValue);
141     m_ui->m_floatValueEditor->setMaximum( maxValue);
142 }
143 
setValue(qreal value)144 void DecimalInputHandler::setValue(qreal value)
145 {
146     value = qAbs(value);
147 
148     m_ui->m_floatValueEditor->setValue(value);
149 }
150 
handleIntEditChange()151 void DecimalInputHandler::handleIntEditChange()
152 {
153     // nothing to do, perhaps rather disconnect the signal with this notation
154 }
155 
handleUIntEditChange()156 void DecimalInputHandler::handleUIntEditChange()
157 {
158     // nothing to do, perhaps rather disconnect the signal with this notation
159 }
160 
handleFloatEditChange()161 void DecimalInputHandler::handleFloatEditChange()
162 {
163     // nothing to do, perhaps rather disconnect the signal with this notation
164 }
165 
calculateValue() const166 qreal DecimalInputHandler::calculateValue() const
167 {
168     qreal value = m_ui->m_floatValueEditor->value();
169 
170     if (m_ui->m_sign->currentIndex() == NegativeSphereIndex) {
171         value *= -1;
172     }
173 
174     return value;
175 }
176 
setupUi()177 void DMSInputHandler::setupUi()
178 {
179     m_ui->m_uintValueEditor->setSuffix(LatLonEdit::tr("'"));
180     m_ui->m_floatValueEditor->setSuffix(LatLonEdit::tr("\""));
181     m_ui->m_floatValueEditor->setDecimals(2);
182 
183     m_ui->m_intValueEditor->show();
184     m_ui->m_uintValueEditor->show();
185 }
186 
setupMinMax(LatLonEdit::Dimension dimension)187 void DMSInputHandler::setupMinMax(LatLonEdit::Dimension dimension)
188 {
189     const int maxValue = (dimension == LatLonEdit::Longitude) ? 180 : 90;
190 
191     m_ui->m_intValueEditor->setMinimum(-maxValue);
192     m_ui->m_intValueEditor->setMaximum( maxValue);
193 }
194 
setValue(qreal value)195 void DMSInputHandler::setValue(qreal value)
196 {
197     value = qAbs( value );
198 
199     int degValue = (int) value;
200 
201     qreal minFValue = 60 * (value - degValue);
202     int minValue = (int) minFValue;
203     qreal secFValue = 60 * (minFValue - minValue);
204     // Adjustment for fuzziness (like 49.999999999999999999999)
205     int secValue = qRound(secFValue);
206     if (secValue > 59) {
207         secFValue = 0.0;
208         ++minValue;
209     }
210     if (minValue > 59) {
211         minValue = 0;
212         ++degValue;
213     }
214 
215     m_ui->m_intValueEditor->setValue( degValue );
216     m_ui->m_uintValueEditor->setValue( minValue );
217     m_ui->m_floatValueEditor->setValue( secFValue );
218 }
219 
handleIntEditChange()220 void DMSInputHandler::handleIntEditChange()
221 {
222     const int degValue = m_ui->m_intValueEditor->value();
223     const int minDegValue = m_ui->m_intValueEditor->minimum();
224     const int maxDegValue = m_ui->m_intValueEditor->maximum();
225     // at max/min?
226     if (degValue <= minDegValue || maxDegValue <= degValue) {
227         m_ui->m_uintValueEditor->setValue( 0 );
228         m_ui->m_floatValueEditor->setValue( 0.0 );
229     }
230 }
231 
handleUIntEditChange()232 void DMSInputHandler::handleUIntEditChange()
233 {
234     const int degValue = m_ui->m_intValueEditor->value();
235     const int minValue = m_ui->m_uintValueEditor->value();
236 
237     if (minValue < 0) {
238         if (degValue != 0) {
239             m_ui->m_uintValueEditor->setValue( 59 );
240             const int degDec = (degValue > 0) ? 1 : -1;
241             m_ui->m_intValueEditor->setValue( degValue - degDec );
242         } else {
243             switchSign( m_ui->m_sign );
244             m_ui->m_uintValueEditor->setValue( 1 );
245         }
246     } else {
247         const int minDegValue = m_ui->m_intValueEditor->minimum();
248         const int maxDegValue = m_ui->m_intValueEditor->maximum();
249         // at max/min already?
250         if (degValue <= minDegValue || maxDegValue <= degValue) {
251             // ignore
252             m_ui->m_uintValueEditor->setValue( 0 );
253         // overflow?
254         } else if (minValue >= 60) {
255             m_ui->m_uintValueEditor->setValue( 0 );
256             // will reach max/min?
257             if (minDegValue+1 == degValue || degValue == maxDegValue-1) {
258                 // reset also sec
259                 m_ui->m_floatValueEditor->setValue( 0.0 );
260             }
261             const int degInc = (degValue > 0) ? 1 : -1;
262             m_ui->m_intValueEditor->setValue( degValue + degInc );
263         }
264     }
265 }
266 
handleFloatEditChange()267 void DMSInputHandler::handleFloatEditChange()
268 {
269     const int degValue = m_ui->m_intValueEditor->value();
270     const int minValue = m_ui->m_uintValueEditor->value();
271     const qreal secValue = m_ui->m_floatValueEditor->value();
272 
273     if (secValue < 0.0) {
274         const qreal secDiff = -secValue;
275         if (degValue == 0 && minValue == 0) {
276             switchSign( m_ui->m_sign );
277             m_ui->m_floatValueEditor->setValue( secDiff );
278         } else {
279             m_ui->m_floatValueEditor->setValue( 60.0 - secDiff );
280             if (minValue > 0) {
281                 m_ui->m_uintValueEditor->setValue( minValue - 1 );
282             } else {
283                 m_ui->m_uintValueEditor->setValue( 59 );
284                 const int degDec = (degValue > 0) ? 1 : -1;
285                 m_ui->m_intValueEditor->setValue( degValue - degDec );
286             }
287         }
288     } else {
289         const int minDegValue = m_ui->m_intValueEditor->minimum();
290         const int maxDegValue = m_ui->m_intValueEditor->maximum();
291         // at max/min already?
292         if (degValue <= minDegValue || maxDegValue <= degValue) {
293             // ignore
294             m_ui->m_floatValueEditor->setValue( 0.0 );
295         // need to inc minutes?
296         } else if (secValue >= 60.0) {
297             qreal newSec = secValue - 60.0;
298             // will reach max/min?
299             if (minValue == 59) {
300                 m_ui->m_uintValueEditor->setValue( 0 );
301                 // will reach max/min?
302                 if (minDegValue+1 == degValue || degValue == maxDegValue-1) {
303                     // reset also sec
304                     newSec = 0.0;
305                 }
306                 const int degInc = (degValue > 0) ? 1 : -1;
307                 m_ui->m_intValueEditor->setValue( degValue + degInc );
308             } else {
309                 m_ui->m_uintValueEditor->setValue( minValue + 1 );
310             }
311             m_ui->m_floatValueEditor->setValue( newSec );
312         }
313     }
314 }
315 
calculateValue() const316 qreal DMSInputHandler::calculateValue() const
317 {
318     const bool isNegativeDeg = ( m_ui->m_intValueEditor->value() < 0 );
319 
320     const qreal deg = (qreal)(qAbs(m_ui->m_intValueEditor->value()));
321     const qreal min = (qreal)(m_ui->m_uintValueEditor->value()) / 60.0;
322     const qreal sec = m_ui->m_floatValueEditor->value() / 3600.0;
323 
324     qreal value = deg + min + sec;
325 
326     if (isNegativeDeg) {
327         value *= -1;
328     }
329     if (m_ui->m_sign->currentIndex() == NegativeSphereIndex) {
330         value *= -1;
331     }
332 
333     return value;
334 }
335 
setupUi()336 void DMInputHandler::setupUi()
337 {
338     m_ui->m_floatValueEditor->setSuffix(LatLonEdit::tr("'"));
339     m_ui->m_floatValueEditor->setDecimals(2);
340 
341     m_ui->m_intValueEditor->show();
342     m_ui->m_uintValueEditor->hide();
343 }
344 
setupMinMax(LatLonEdit::Dimension dimension)345 void DMInputHandler::setupMinMax(LatLonEdit::Dimension dimension)
346 {
347     const int maxValue = (dimension == LatLonEdit::Longitude) ? 180 : 90;
348 
349     m_ui->m_intValueEditor->setMinimum(-maxValue);
350     m_ui->m_intValueEditor->setMaximum( maxValue);
351 }
352 
setValue(qreal value)353 void DMInputHandler::setValue(qreal value)
354 {
355     value = qAbs(value);
356 
357     int degValue = (int)value;
358 
359     qreal minFValue = 60 * (value - degValue);
360     // Adjustment for fuzziness (like 49.999999999999999999999)
361     int minValue = qRound( minFValue );
362     if (minValue > 59) {
363         minFValue = 0.0;
364         ++degValue;
365     }
366 
367     m_ui->m_intValueEditor->setValue( degValue );
368     m_ui->m_floatValueEditor->setValue( minFValue );
369 }
370 
handleIntEditChange()371 void DMInputHandler::handleIntEditChange()
372 {
373     const int degValue = m_ui->m_intValueEditor->value();
374     const int minDegValue = m_ui->m_intValueEditor->minimum();
375     const int maxDegValue = m_ui->m_intValueEditor->maximum();
376     // at max/min?
377     if (degValue <= minDegValue || maxDegValue <= degValue) {
378         m_ui->m_floatValueEditor->setValue( 0.0 );
379     }
380 }
381 
handleUIntEditChange()382 void DMInputHandler::handleUIntEditChange()
383 {
384     // nothing to be done here, should be never called
385 }
386 
handleFloatEditChange()387 void DMInputHandler::handleFloatEditChange()
388 {
389     const int degValue = m_ui->m_intValueEditor->value();
390     const qreal minValue = m_ui->m_floatValueEditor->value();
391 
392     if (minValue < 0.0) {
393         const qreal minDiff = -minValue;
394         if (degValue == 0) {
395             switchSign( m_ui->m_sign );
396             m_ui->m_floatValueEditor->setValue( minDiff );
397         } else {
398             m_ui->m_floatValueEditor->setValue( 60.0 - minDiff );
399             m_ui->m_intValueEditor->setValue( degValue - 1 );
400         }
401     } else {
402         const int minDegValue = m_ui->m_intValueEditor->minimum();
403         const int maxDegValue = m_ui->m_intValueEditor->maximum();
404         // at max/min already?
405         if (degValue <= minDegValue || maxDegValue <= degValue) {
406             // ignore
407             m_ui->m_floatValueEditor->setValue( 0.0 );
408         // need to inc degrees?
409         } else if (minValue >= 60.0) {
410             qreal newMin = minValue - 60.0;
411             // will reach max/min?
412             if (minDegValue+1 == degValue || degValue == maxDegValue-1) {
413                 // reset also sec
414                 newMin = 0.0;
415             } else {
416                 m_ui->m_intValueEditor->setValue( degValue + 1 );
417             }
418             m_ui->m_floatValueEditor->setValue( newMin );
419         }
420     }
421 }
422 
calculateValue() const423 qreal DMInputHandler::calculateValue() const
424 {
425     const bool isNegativeDeg = ( m_ui->m_intValueEditor->value() < 0 );
426 
427     const qreal deg = (qreal)(qAbs(m_ui->m_intValueEditor->value()));
428     const qreal min = m_ui->m_floatValueEditor->value() / 60.0;
429 
430     qreal value = deg + min;
431 
432     if (isNegativeDeg) {
433         value *= -1;
434     }
435     if (m_ui->m_sign->currentIndex() == NegativeSphereIndex) {
436         value *= -1;
437     }
438 
439     return value;
440 }
441 
442 
LatLonEditPrivate()443 LatLonEditPrivate::LatLonEditPrivate()
444     : m_dimension(LatLonEdit::Latitude)
445     , m_value(0.0)
446     , m_notation(GeoDataCoordinates::DMS)
447     , m_inputHandler(new DMSInputHandler(this))
448     , m_updating(false)
449 {}
450 
~LatLonEditPrivate()451 LatLonEditPrivate::~LatLonEditPrivate()
452 {
453     delete m_inputHandler;
454 }
455 
init(QWidget * parent)456 void LatLonEditPrivate::init(QWidget* parent) { setupUi(parent); }
457 
458 }
459 
460 
461 using namespace Marble;
462 
LatLonEdit(QWidget * parent,LatLonEdit::Dimension dimension,GeoDataCoordinates::Notation notation)463 LatLonEdit::LatLonEdit(QWidget *parent, LatLonEdit::Dimension dimension, GeoDataCoordinates::Notation notation)
464     : QWidget( parent ), d(new LatLonEditPrivate())
465 {
466     d->init(this);
467     setDimension(dimension);
468     setNotation(notation);
469 
470     connect(d->m_intValueEditor,   SIGNAL(valueChanged(int)),    this, SLOT(checkIntValueOverflow()));
471     connect(d->m_uintValueEditor,  SIGNAL(valueChanged(int)),    this, SLOT(checkUIntValueOverflow()));
472     connect(d->m_floatValueEditor, SIGNAL(valueChanged(double)), this, SLOT(checkFloatValueOverflow()));
473 
474     connect(d->m_sign, SIGNAL(currentIndexChanged(int)),
475                  this, SLOT(onSignChanged()));
476 }
477 
~LatLonEdit()478 LatLonEdit::~LatLonEdit()
479 {
480     delete d;
481 }
482 
value() const483 qreal LatLonEdit::value() const
484 {
485     return d->m_value;
486 }
487 
notation() const488 GeoDataCoordinates::Notation LatLonEdit::notation() const
489 {
490     return d->m_notation;
491 }
492 
onSignChanged()493 void LatLonEdit::onSignChanged()
494 {
495     if( d->m_updating )
496         return;
497 
498     // Only flip the value if it does not match the sign
499     if (d->m_sign->currentIndex() == PositiveSphereIndex) {
500         if (d->m_value < 0.0) {
501             d->m_value *= -1;
502         }
503     } else {
504         if (d->m_value > 0.0) {
505             d->m_value *= -1;
506         }
507     }
508 
509     emit valueChanged( d->m_value );
510 }
511 
setDimension(Dimension dimension)512 void LatLonEdit::setDimension( Dimension dimension )
513 {
514     d->m_dimension = dimension;
515 
516     d->m_updating = true;
517 
518     d->m_inputHandler->setupMinMax(dimension);
519 
520     // update sign widget content
521     {
522         d->m_sign->clear();
523 
524         switch (dimension) {
525         case Longitude:
526             d->m_sign->addItem( tr("E", "East, the direction" ) );
527             d->m_sign->addItem( tr("W", "West, the direction" ) );
528             break;
529         case Latitude:
530             d->m_sign->addItem( tr("N", "North, the direction" ) );
531             d->m_sign->addItem( tr("S", "South, the direction" ) );
532             break;
533         }
534     }
535 
536     d->m_updating = false;
537 
538     // reset value, old one is useless
539     setValue( 0.0 );
540 }
541 
setNotation(GeoDataCoordinates::Notation notation)542 void LatLonEdit::setNotation(GeoDataCoordinates::Notation notation)
543 {
544     delete d->m_inputHandler;
545     d->m_inputHandler = nullptr;
546 
547     switch (notation) {
548     case GeoDataCoordinates::Decimal:
549         d->m_inputHandler = new DecimalInputHandler(d);
550         break;
551     case GeoDataCoordinates::DMS:
552         d->m_inputHandler = new DMSInputHandler(d);
553         break;
554     case GeoDataCoordinates::DM:
555         d->m_inputHandler = new DMInputHandler(d);
556         break;
557     case GeoDataCoordinates::UTM:
558         /** @todo see below */
559         break;
560     case GeoDataCoordinates::Astro:
561         /** @todo see below */
562         break;
563     }
564 
565     if (!d->m_inputHandler) {
566         /** @todo Temporary fallback to DecimalInputHandler
567          *        Implement proper handlers for UTM and Astro */
568         d->m_inputHandler = new DecimalInputHandler(d);
569     }
570 
571     d->m_notation = notation;
572     d->m_inputHandler->setupUi();
573     d->m_inputHandler->setupMinMax(d->m_dimension);
574     d->m_inputHandler->setValue(d->m_value);
575 }
576 
checkFloatValueOverflow()577 void LatLonEdit::checkFloatValueOverflow()
578 {
579     if( d->m_updating )
580         return;
581 
582     d->m_updating = true;
583 
584     d->m_inputHandler->handleFloatEditChange();
585 
586     d->m_updating = false;
587 
588     recalculate();
589 }
590 
591 
checkUIntValueOverflow()592 void LatLonEdit::checkUIntValueOverflow()
593 {
594     if( d->m_updating )
595         return;
596 
597     d->m_updating = true;
598 
599     d->m_inputHandler->handleUIntEditChange();
600 
601     d->m_updating = false;
602 
603     recalculate();
604 }
605 
checkIntValueOverflow()606 void LatLonEdit::checkIntValueOverflow()
607 {
608     if( d->m_updating )
609         return;
610 
611     d->m_updating = true;
612 
613     d->m_inputHandler->handleIntEditChange();
614 
615     d->m_updating = false;
616 
617     recalculate();
618 }
619 
setValue(qreal value)620 void LatLonEdit::setValue( qreal value )
621 {
622     // limit to allowed values
623     const qreal maxValue = (d->m_dimension == Longitude) ? 180.0 : 90.0;
624 
625     if (value > maxValue) {
626         value = maxValue;
627     } else {
628         const qreal minValue = -maxValue;
629         if (value < minValue) {
630             value = minValue;
631         }
632     }
633 
634     // no change?
635     if( value == d->m_value ) {
636         return;
637     }
638 
639     d->m_value = value;
640 
641     // calculate sub values
642     // calculation is done similar to GeoDataCoordinates::lonToString,
643     // perhaps should be moved with similar methods into some utility class/namespace
644 
645     d->m_updating = true;
646 
647     d->m_inputHandler->setValue(value);
648 
649     const bool isNegative = (value < 0.0);
650     d->m_sign->setCurrentIndex( isNegative ? NegativeSphereIndex : PositiveSphereIndex );
651 
652     d->m_updating = false;
653 }
654 
recalculate()655 void LatLonEdit::recalculate()
656 {
657     d->m_value = d->m_inputHandler->calculateValue();
658 
659     emit valueChanged( d->m_value );
660 }
661 
662 
663 #include "moc_LatLonEdit.cpp"
664