1 /***************************************************************************
2 * Copyright (C) 2008 Matthew Gates *
3 * Copyright (C) 2015 Georg Zotti (min/max limits) *
4 * *
5 * This program is free software; you can redistribute it and/or modify *
6 * it under the terms of the GNU General Public License as published by *
7 * the Free Software Foundation; either version 2 of the License, or *
8 * (at your option) any later version. *
9 * *
10 * This program is distributed in the hope that it will be useful, *
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13 * GNU General Public License for more details. *
14 * *
15 * You should have received a copy of the GNU General Public License *
16 * along with this program; if not, write to the *
17 * Free Software Foundation, Inc., *
18 * 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA. *
19 ***************************************************************************/
20
21 #include "AngleSpinBox.hpp"
22 #include "StelTranslator.hpp"
23 #include <QDebug>
24 #include <QString>
25 #include <QLineEdit>
26 #include <QWidget>
27 #include <QLocale>
28 #include <limits>
29 #include <QRegularExpression>
30
AngleSpinBox(QWidget * parent,DisplayFormat format,PrefixType prefix)31 AngleSpinBox::AngleSpinBox(QWidget* parent, DisplayFormat format, PrefixType prefix)
32 : QAbstractSpinBox(parent),
33 angleSpinBoxFormat(format),
34 currentPrefixType(prefix),
35 decimalPlaces(2),
36 radAngle(0.0),
37 minRad(-std::numeric_limits<double>::max()),
38 maxRad( std::numeric_limits<double>::max())
39 {
40 connect(this, SIGNAL(editingFinished()), this, SLOT(updateValue()));
41 formatText();
42 setWrapping(false); // but should be set true for longitudinal cycling
43 }
44
positivePrefix(PrefixType prefix)45 const QString AngleSpinBox::positivePrefix(PrefixType prefix)
46 {
47 switch(prefix)
48 {
49 case NormalPlus:
50 return("+");
51 case Longitude:
52 return(q_("E")+" ");
53 case Latitude:
54 return(q_("N")+" ");
55 case Normal:
56 default:
57 return("");
58 }
59 }
60
negativePrefix(PrefixType prefix)61 const QString AngleSpinBox::negativePrefix(PrefixType prefix)
62 {
63 switch(prefix)
64 {
65 case NormalPlus:
66 return(QLocale().negativeSign());
67 case Longitude:
68 return(q_("W")+" ");
69 case Latitude:
70 return(q_("S")+" ");
71 case Normal:
72 default:
73 return(QLocale().negativeSign());
74 }
75 }
76
~AngleSpinBox()77 AngleSpinBox::~AngleSpinBox()
78 {
79 }
80
81
getCurrentSection() const82 AngleSpinBox::AngleSpinboxSection AngleSpinBox::getCurrentSection() const
83 {
84 int cursorPos = lineEdit()->cursorPosition();
85 const QString str = lineEdit()->text();
86
87 // Regexp must not have "+-" immediately behind "[" !
88 int cPosMin = str.indexOf(QRegularExpression("^["+q_("N")+q_("S")+q_("E")+q_("W")+"+-]"), 0);
89 // without prefix (e.g. right ascension): avoid unwanted negating!
90 if ((cPosMin==-1) && (cursorPos==0)) {
91 return SectionDegreesHours;
92 }
93 int cPosMax = cPosMin+1;
94
95 if (cursorPos>=cPosMin && cursorPos<cPosMax) {
96 return SectionPrefix;
97 }
98
99 cPosMin = cPosMax;
100 cPosMax = str.indexOf(QRegularExpression(QString("[dh%1]").arg(QChar(176))), 0)+1;
101 if (cursorPos >= cPosMin && cursorPos <= cPosMax) {
102 return SectionDegreesHours;
103 }
104
105 cPosMin = cPosMax;
106 cPosMax = str.indexOf(QRegularExpression("[m']"), 0)+1;
107 if (cursorPos > cPosMin && cursorPos <= cPosMax) {
108 return SectionMinutes;
109 }
110
111 cPosMin = cPosMax;
112 cPosMax = str.indexOf(QRegularExpression("[s\"]"), 0)+1;
113 if (cursorPos > cPosMin && cursorPos <= cPosMax) {
114 return SectionSeconds;
115 }
116
117 return SectionNone;
118 }
119
stepBy(int steps)120 void AngleSpinBox::stepBy (int steps)
121 {
122 const int cursorPos = lineEdit()->cursorPosition();
123 const AngleSpinBox::AngleSpinboxSection sec = getCurrentSection();
124 switch (sec)
125 {
126 case SectionPrefix:
127 {
128 radAngle = -radAngle;
129 break;
130 }
131 case SectionDegreesHours:
132 {
133 if (angleSpinBoxFormat==DMSLetters || angleSpinBoxFormat==DMSSymbols || angleSpinBoxFormat==DecimalDeg
134 || angleSpinBoxFormat==DMSLettersUnsigned || angleSpinBoxFormat==DMSSymbolsUnsigned )
135 radAngle += M_PI/180.*steps;
136 else
137 radAngle += M_PI/12.*steps;
138 break;
139 }
140 case SectionMinutes:
141 {
142 if (angleSpinBoxFormat==DMSLetters || angleSpinBoxFormat==DMSSymbols || angleSpinBoxFormat==DecimalDeg
143 || angleSpinBoxFormat==DMSLettersUnsigned || angleSpinBoxFormat==DMSSymbolsUnsigned )
144 radAngle += M_PI/180.*steps/60.;
145 else
146 radAngle += M_PI/12.*steps/60.;
147 break;
148 }
149 case SectionSeconds:
150 case SectionNone:
151 {
152 if (angleSpinBoxFormat==DMSLetters || angleSpinBoxFormat==DMSSymbols || angleSpinBoxFormat==DecimalDeg
153 || angleSpinBoxFormat==DMSLettersUnsigned || angleSpinBoxFormat==DMSSymbolsUnsigned )
154 radAngle += M_PI/180.*steps/3600.;
155 else
156 radAngle += M_PI/12.*steps/3600.;
157 break;
158 }
159 default:
160 {
161 return;
162 }
163 }
164 if (wrapping())
165 {
166 if (radAngle > maxRad)
167 radAngle=minRad+(radAngle-maxRad);
168 else if (radAngle < minRad)
169 radAngle=maxRad-(minRad-radAngle);
170 }
171 radAngle=qMin(radAngle, maxRad);
172 radAngle=qMax(radAngle, minRad);
173 formatText();
174 lineEdit()->setCursorPosition(cursorPos);
175 emit(valueChanged());
176 emit(valueChangedDeg(valueDegrees()));
177 emit(valueChangedRad(valueRadians()));
178 }
179
validate(QString & input,int & pos) const180 QValidator::State AngleSpinBox::validate(QString& input, int& pos) const
181 {
182 Q_UNUSED(pos);
183 QValidator::State state;
184 stringToDouble(input, &state);
185 return state;
186 }
187
clear()188 void AngleSpinBox::clear()
189 {
190 radAngle = 0.0;
191 formatText();
192 emit(valueChanged());
193 emit(valueChangedDeg(valueDegrees()));
194 emit(valueChangedRad(valueRadians()));
195 }
196
stepEnabled() const197 QAbstractSpinBox::StepEnabled AngleSpinBox::stepEnabled() const
198 {
199 return (StepUpEnabled|StepDownEnabled);
200 }
201
valueRadians() const202 double AngleSpinBox::valueRadians() const
203 {
204 return radAngle;
205 }
206
valueDegrees() const207 double AngleSpinBox::valueDegrees() const
208 {
209 return radAngle*(180./M_PI);
210 }
211
stringToDouble(QString input,QValidator::State * state,PrefixType prefix) const212 double AngleSpinBox::stringToDouble(QString input, QValidator::State* state, PrefixType prefix) const
213 {
214 if (prefix==Unknown)
215 {
216 prefix=currentPrefixType;
217 }
218 int sign=1;
219 if (input.startsWith(negativePrefix(prefix), Qt::CaseInsensitive))
220 {
221 sign = -1;
222 input = input.mid(negativePrefix(prefix).length());
223 }
224 else if (input.startsWith(positivePrefix(prefix), Qt::CaseInsensitive))
225 {
226 sign = 1;
227 input = input.mid(positivePrefix(prefix).length());
228 }
229 else if (input.startsWith("-", Qt::CaseInsensitive))
230 {
231 sign = -1;
232 input = input.mid(1);
233 }
234 else if (input.startsWith("+", Qt::CaseInsensitive))
235 {
236 sign = 1;
237 input = input.mid(1);
238 }
239
240 QRegularExpression dmsRx("^\\s*(\\d+)\\s*[d\\x00b0](\\s*(\\d+(\\.\\d*)?)\\s*[m'](\\s*(\\d+(\\.\\d*)?)\\s*[s\"]\\s*)?)?$",
241 QRegularExpression::CaseInsensitiveOption);
242 QRegularExpression hmsRx("^\\s*(\\d+)\\s*h(\\s*(\\d+(\\.\\d*)?)\\s*[m'](\\s*(\\d+(\\.\\d*)?)\\s*[s\"]\\s*)?)?$",
243 QRegularExpression::CaseInsensitiveOption);
244 QRegularExpression decRx("^(\\d+(\\.\\d*)?)(\\s*[\\x00b0]\\s*)?$");
245 QRegularExpression badRx("[^hdms0-9 \\x00b0'\"\\.]", QRegularExpression::CaseInsensitiveOption);
246 QRegularExpressionMatch dmsMatch=dmsRx.match(input);
247 QRegularExpressionMatch hmsMatch=hmsRx.match(input);
248 QRegularExpressionMatch decMatch=decRx.match(input);
249
250 QValidator::State dummy;
251 if (state == Q_NULLPTR)
252 {
253 state = &dummy;
254 }
255
256 if (dmsMatch.hasMatch())
257 {
258 double degree = dmsMatch.captured(1).toDouble();
259 double minute = dmsMatch.captured(3).toDouble();
260 double second = dmsMatch.captured(6).toDouble();
261 if (degree > 360.0 || degree < -360.0)
262 {
263 *state = QValidator::Invalid;
264 return 0.0;
265 }
266
267 if (minute > 60.0 || minute < 0.0)
268 {
269 *state = QValidator::Invalid;
270 return 0.0;
271 }
272
273 if (second > 60.0 || second < 0.0)
274 {
275 *state = QValidator::Invalid;
276 return 0.0;
277 }
278
279 *state = QValidator::Acceptable;
280 return (sign * (degree + (minute/60.0) + (second/3600.0))) * M_PI / 180.0;
281 }
282 else if (hmsMatch.hasMatch())
283 {
284 double hour = hmsMatch.captured(1).toDouble();
285 double minute = hmsMatch.captured(3).toDouble();
286 double second = hmsMatch.captured(6).toDouble();
287 if (hour >= 24.0 || hour < 0.0)
288 {
289 *state = QValidator::Invalid;
290 return 0.0;
291 }
292
293 if (minute > 60.0 || minute < 0.0)
294 {
295 *state = QValidator::Invalid;
296 return 0.0;
297 }
298
299 if (second > 60.0 || second < 0.0)
300 {
301 *state = QValidator::Invalid;
302 return 0.0;
303 }
304
305 *state = QValidator::Acceptable;
306 return sign * (((360.0*hour/24.0) + (minute*15/60.0) + (second*15/3600.0)) * M_PI / 180.0);
307 }
308 else if (decMatch.hasMatch())
309 {
310 double dec = decMatch.captured(1).toDouble();
311 if (dec < 0.0 || dec > 360.0)
312 {
313 *state = QValidator::Invalid;
314 return 0.0;
315 }
316 else
317 {
318 *state = QValidator::Acceptable;
319 return sign * (dec * M_PI / 180.0);
320 }
321 }
322 else if (input.contains(badRx))
323 {
324 *state = QValidator::Invalid;
325 return 0.0;
326 }
327 *state = QValidator::Intermediate;
328 return 0.0;
329 }
330
updateValue(void)331 void AngleSpinBox::updateValue(void)
332 {
333 QValidator::State state;
334 double a = stringToDouble(lineEdit()->text(), &state);
335 if (state != QValidator::Acceptable)
336 return;
337
338 if (qFuzzyCompare(radAngle, a))
339 return;
340 radAngle = a;
341
342 if (wrapping())
343 {
344 if (radAngle > maxRad)
345 radAngle=minRad+(radAngle-maxRad);
346 else if (radAngle < minRad)
347 radAngle=maxRad-(minRad-radAngle);
348 }
349 radAngle=qMin(radAngle, maxRad);
350 radAngle=qMax(radAngle, minRad);
351
352 formatText();
353 emit(valueChanged());
354 emit(valueChangedDeg(valueDegrees()));
355 emit(valueChangedRad(valueRadians()));
356 }
357
setRadians(double radians)358 void AngleSpinBox::setRadians(double radians)
359 {
360 radAngle = radians;
361 formatText();
362 }
363
setDegrees(double degrees)364 void AngleSpinBox::setDegrees(double degrees)
365 {
366 radAngle = degrees * M_PI/180.;
367 formatText();
368 }
369
formatText(void)370 void AngleSpinBox::formatText(void)
371 {
372 const int cursorPos=lineEdit()->cursorPosition();
373 switch (angleSpinBoxFormat)
374 {
375 case DMSLetters:
376 case DMSSymbols:
377 case DMSLettersUnsigned:
378 case DMSSymbolsUnsigned:
379 {
380 double angle = radAngle;
381 int d, m;
382 double s;
383 bool sign=true;
384 if (angle<0)
385 {
386 angle *= -1;
387 sign = false;
388 }
389 angle = fmod(angle,2.0*M_PI);
390 angle *= 180./M_PI;
391
392 if ( (!sign) && ( (angleSpinBoxFormat==DMSLettersUnsigned) || (angleSpinBoxFormat==DMSSymbolsUnsigned))) {
393 angle = 360.0-angle;
394 sign=true;
395 }
396
397 d = static_cast<int>(angle);
398 m = static_cast<int>((angle - d)*60);
399 s = (angle-d)*3600-60*m;
400
401 // we may have seconds as 60 and one less minute...
402 if (s > 60.0 - ::pow(10.0, -1 * (decimalPlaces+1)))
403 {
404 m+=1;
405 s-=60.0;
406 }
407
408 // may have to carry to the degrees...
409 if (m >= 60)
410 {
411 d= (d+1) % 360;
412 m-=60;
413 }
414
415 // fix when we have tiny tiny tiny values.
416 if (abs(s) < ::pow(10.0, -1 * (decimalPlaces+1)))
417 s= 0.0;
418
419 QString signInd = positivePrefix(currentPrefixType);
420 if (!sign)
421 signInd = negativePrefix(currentPrefixType);
422
423 if ((angleSpinBoxFormat == DMSLetters) || (angleSpinBoxFormat == DMSLettersUnsigned))
424 lineEdit()->setText(QString("%1%2d %3m %4s")
425 .arg(signInd).arg(d).arg(m).arg(s, 0, 'f', decimalPlaces, ' '));
426 else
427 lineEdit()->setText(QString("%1%2%3 %4' %5\"")
428 .arg(signInd).arg(d).arg(QChar(176)).arg(m)
429 .arg(s, 0, 'f', decimalPlaces, ' '));
430 break;
431 }
432 case HMSLetters:
433 case HMSSymbols:
434 {
435 unsigned int h, m;
436 double s;
437 double angle = radAngle;
438 angle = fmod(angle,2.0*M_PI);
439 if (angle < 0.0) angle += 2.0*M_PI; // range: [0..2.0*M_PI)
440 angle *= 12./M_PI;
441 h = static_cast<unsigned int>(angle);
442 m = static_cast<unsigned int>((angle-h)*60);
443 s = (angle-h)*3600.-60.*m;
444
445 // we may have seconds as 60 and one less minute...
446 if (s > 60.0 - ::pow(10.0, -1 * (decimalPlaces+1)))
447 {
448 m+=1;
449 s-=60.0;
450 }
451
452 // may have to carry to the degrees...
453 if (m >= 60)
454 {
455 h = (h+1) % 24;
456 m-=60;
457 }
458
459 // fix when we have tiny tiny tiny values.
460 if (abs(s) < ::pow(10.0, -1 * (decimalPlaces+1)))
461 s= 0.0;
462
463 if (angleSpinBoxFormat == HMSLetters)
464 lineEdit()->setText(QString("%1h %2m %3s")
465 .arg(h).arg(m).arg(s, 0, 'f', decimalPlaces, ' '));
466 else
467 lineEdit()->setText(QString("%1h %2' %3\"")
468 .arg(h).arg(m).arg(s, 0, 'f', decimalPlaces, ' '));
469 break;
470 }
471 case DecimalDeg:
472 {
473 double angle = radAngle;
474 QString signInd = positivePrefix(currentPrefixType);
475
476 if (radAngle<0)
477 {
478 angle *= -1;
479 signInd = negativePrefix(currentPrefixType);
480 }
481
482 lineEdit()->setText(QString("%1%2%3")
483 .arg(signInd)
484 .arg(fmod(angle * 180.0 / M_PI, 360.0), 0, 'f', decimalPlaces, ' ')
485 .arg(QChar(176)));
486 break;
487 }
488 default:
489 {
490 qWarning() << "AngleSpinBox::formatText - WARNING - unknown format"
491 << static_cast<int>(angleSpinBoxFormat);
492 break;
493 }
494 }
495 lineEdit()->setCursorPosition(cursorPos);
496 }
497
498