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