1 /*
2     SPDX-FileCopyrightText: 2008 Jason Harris <kstars@30doradus.org>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "calendarwidget.h"
8 
9 #include "ksalmanac.h"
10 #include "kssun.h"
11 #include "kstarsdata.h"
12 #include "skycalendar.h"
13 
14 #include <KLocalizedString>
15 #include <KPlotting/KPlotObject>
16 
17 #include <QPainter>
18 #include <QDebug>
19 
20 #define BIGTICKSIZE   10
21 #define SMALLTICKSIZE 4
22 
CalendarWidget(QWidget * parent)23 CalendarWidget::CalendarWidget(QWidget *parent) : KPlotWidget(parent)
24 {
25     setAntialiasing(true);
26 
27     setTopPadding(40);
28     setLeftPadding(60);
29     setRightPadding(100);
30 
31     maxRTime = 12.0;
32     minSTime = -12.0;
33 }
34 
paintEvent(QPaintEvent * e)35 void CalendarWidget::paintEvent(QPaintEvent *e)
36 {
37     Q_UNUSED(e)
38 
39     QPainter p;
40 
41     p.begin(this);
42     p.setRenderHint(QPainter::Antialiasing, antialiasing());
43     p.fillRect(rect(), backgroundColor());
44     p.translate(leftPadding(), topPadding());
45 
46     setPixRect();
47     p.setClipRect(pixRect());
48     p.setClipping(true);
49 
50     drawHorizon(&p);
51 
52     foreach (KPlotObject *po, plotObjects())
53     {
54         po->draw(&p, this);
55     }
56 
57     p.setClipping(false);
58     drawAxes(&p);
59 }
60 
setHorizon()61 void CalendarWidget::setHorizon()
62 {
63     KSSun thesun;
64     SkyCalendar *skycal = (SkyCalendar *)topLevelWidget();
65     KStarsDateTime kdt(QDate(skycal->year(), 1, 1), QTime(12, 0, 0));
66 
67     maxRTime = 0.0;
68     minSTime = 0.0;
69 
70     // Clear date, rise and set time lists
71     dateList.clear();
72     riseTimeList.clear();
73     setTimeList.clear();
74 
75     float rTime, sTime;
76 
77     // Get rise and set time every 7 days for 1 year
78     while (skycal->year() == kdt.date().year())
79     {
80         QTime tmp_rTime = thesun.riseSetTime(KStarsDateTime(kdt.djd() + 1.0), skycal->get_geo(), true, true);
81         QTime tmp_sTime = thesun.riseSetTime(KStarsDateTime(kdt.djd()), skycal->get_geo(), false, true);
82 
83         /* riseSetTime seems buggy since it sometimes returns the same time for rise and set (01:00:00).
84          * In this case, we just reset tmp_rTime and tmp_sTime so they will be considered invalid
85          * in the following lines.
86          * NOTE: riseSetTime should be fix now, this test is no longer necessary*/
87         if (tmp_rTime == tmp_sTime)
88         {
89             tmp_rTime = QTime();
90             tmp_sTime = QTime();
91         }
92 
93         // If rise and set times are valid, the sun rise and set...
94         if (tmp_rTime.isValid() && tmp_sTime.isValid())
95         {
96             // Compute X-coordinate value for rise and set time
97             QTime midday(12, 0, 0);
98             rTime = tmp_rTime.secsTo(midday) * 24.0 / 86400.0;
99             sTime = tmp_sTime.secsTo(midday) * 24.0 / 86400.0;
100 
101             if (tmp_rTime <= midday)
102                 rTime = 12.0 - rTime;
103             else
104                 rTime = -12.0 - rTime;
105 
106             if (tmp_sTime <= midday)
107                 sTime = 12.0 - sTime;
108             else
109                 sTime = -12.0 - sTime;
110         }
111         /* else, the sun don't rise and/or don't set.
112          * we look at the altitude of the sun at transit time, if it is above the horizon,
113          * there is no night, else there is no day. */
114         else
115         {
116             if (thesun.transitAltitude(KStarsDateTime(kdt.djd()), skycal->get_geo()).degree() > 0)
117             {
118                 rTime = -4.0;
119                 sTime = 4.0;
120             }
121             else
122             {
123                 rTime = 12.0;
124                 sTime = -12.0;
125             }
126         }
127 
128         // Get max rise time and min set time
129         if (rTime > maxRTime)
130             maxRTime = rTime;
131         if (sTime < minSTime)
132             minSTime = sTime;
133 
134         // Keep the day, rise time and set time in lists
135         dateList.append(kdt.date());
136         riseTimeList.append(rTime);
137         setTimeList.append(sTime);
138 
139         // Next week
140         kdt = kdt.addDays(skycal->scUI->spinBox_Interval->value());
141     }
142 
143     // Set widget limits
144     maxRTime = ceil(maxRTime) + 1.0;
145     if ((int)maxRTime % 2 != 0)
146         maxRTime++;
147     if (maxRTime > 12.0)
148         maxRTime = 12.0;
149     minSTime = floor(minSTime) - 1.0;
150     if ((int)minSTime % 2 != 0)
151         minSTime--;
152     if (minSTime < -12.0)
153         minSTime = -12.0;
154     setLimits(minSTime, maxRTime, 0.0, 366.0);
155     setPixRect();
156 }
157 
drawHorizon(QPainter * p)158 void CalendarWidget::drawHorizon(QPainter *p)
159 {
160     polySunRise.clear();
161     polySunSet.clear();
162 
163     for (int date = 0; date < dateList.size(); date++)
164     {
165         int day = dateList.at(date).daysInYear() - dateList.at(date).dayOfYear();
166         polySunRise << mapToWidget(QPointF(riseTimeList.at(date), day));
167         polySunSet << mapToWidget(QPointF(setTimeList.at(date), day));
168     }
169 
170     //Finish polygons by adding pixRect corners
171     polySunRise << mapToWidget(QPointF(riseTimeList.last(), dataRect().top()))
172                 << mapToWidget(QPointF(dataRect().right(), dataRect().top()))
173                 << mapToWidget(QPointF(dataRect().right(), dataRect().bottom()))
174                 << mapToWidget(QPointF(riseTimeList.first(), dataRect().bottom()));
175     polySunSet << mapToWidget(QPointF(setTimeList.last(), dataRect().top()))
176                << mapToWidget(QPointF(dataRect().left(), pixRect().top()))
177                << mapToWidget(QPointF(dataRect().left(), pixRect().bottom()))
178                << mapToWidget(QPointF(setTimeList.first(), dataRect().bottom()));
179 
180     p->setPen(Qt::darkGreen);
181     p->setBrush(Qt::darkGreen);
182     p->drawPolygon(polySunRise);
183     p->drawPolygon(polySunSet);
184 }
185 
drawAxes(QPainter * p)186 void CalendarWidget::drawAxes(QPainter *p)
187 {
188     SkyCalendar *skycal = (SkyCalendar *)topLevelWidget();
189 
190     p->setPen(foregroundColor());
191     p->setBrush(Qt::NoBrush);
192 
193     //Draw bounding box for the plot
194     p->drawRect(pixRect());
195 
196     //set small font for axis labels
197     QFont f = p->font();
198     int s   = f.pointSize();
199     f.setPointSize(s - 2);
200     p->setFont(f);
201 
202     // Top axis label
203     p->drawText(0, -38, pixRect().width(), pixRect().height(), Qt::AlignHCenter | Qt::AlignTop | Qt::TextDontClip,
204                 i18n("Local time"));
205     // Bottom axis label
206     p->drawText(0, 0, pixRect().width(), pixRect().height() + 35, Qt::AlignHCenter | Qt::AlignBottom | Qt::TextDontClip,
207                 i18n("Universal time"));
208     // Left axis label
209     p->save();
210     p->rotate(90.0);
211     p->drawText(0, 0, pixRect().height(), leftPadding() - 5, Qt::AlignHCenter | Qt::AlignBottom | Qt::TextDontClip,
212                 i18n("Month"));
213     // Right axis label
214     p->translate(0.0, -1 * frameRect().width() + 30);
215     p->drawText(0, 0, pixRect().height(), leftPadding() - 5, Qt::AlignHCenter | Qt::AlignBottom | Qt::TextDontClip,
216                 i18n("Julian date"));
217     p->restore();
218 
219     //Top/Bottom axis tickmarks and time labels
220     for (float xx = minSTime; xx <= maxRTime; xx += 1.0)
221     {
222         int h = int(xx);
223         if (h < 0)
224             h += 24;
225         QTime time(h, 0, 0);
226         QString sTime = QLocale().toString(time, "hh:mm");
227 
228         QString sUtTime = QLocale().toString(time.addSecs(skycal->get_geo()->TZ() * -3600), "hh:mm");
229 
230         // Draw a small tick every hours and a big tick every two hours.
231         QPointF pBottomTick = mapToWidget(QPointF(xx, dataRect().y()));
232         QPointF pTopTick    = QPointF(pBottomTick.x(), 0.0);
233         if (h % 2)
234         {
235             // Draw small bottom tick
236             p->drawLine(pBottomTick, QPointF(pBottomTick.x(), pBottomTick.y() - SMALLTICKSIZE));
237             // Draw small top tick
238             p->drawLine(pTopTick, QPointF(pTopTick.x(), pTopTick.y() + SMALLTICKSIZE));
239         }
240         else
241         {
242             // Draw big bottom tick
243             p->drawLine(pBottomTick, QPointF(pBottomTick.x(), pBottomTick.y() - BIGTICKSIZE));
244             QRectF r(pBottomTick.x() - BIGTICKSIZE, pBottomTick.y() + 0.5 * BIGTICKSIZE, 2 * BIGTICKSIZE, BIGTICKSIZE);
245             p->drawText(r, Qt::AlignCenter | Qt::TextDontClip, sUtTime);
246             // Draw big top tick
247             p->drawLine(pTopTick, QPointF(pTopTick.x(), pTopTick.y() + BIGTICKSIZE));
248             r.moveTop(-2.0 * BIGTICKSIZE);
249             p->drawText(r, Qt::AlignCenter | Qt::TextDontClip, sTime);
250         }
251 
252         // Vertical grid
253         if (skycal->scUI->checkBox_GridVertical->isChecked())
254         {
255             QColor c = p->pen().color();
256             c.setAlpha(100);
257             p->setPen(c);
258             p->drawLine(pTopTick, pBottomTick);
259             c.setAlpha(255);
260             p->setPen(c);
261         }
262     }
263 
264     // Month dividers
265     int y = skycal->year();
266     for (int imonth = 2; imonth <= 12; ++imonth)
267     {
268         QDate dt(y, imonth, 1);
269         float doy = float(dt.daysInYear() - dt.dayOfYear());
270 
271         // Draw a tick every months on left axis
272         QPointF pMonthTick = mapToWidget(QPointF(dataRect().x(), doy));
273         p->drawLine(pMonthTick, QPointF(pMonthTick.x() + BIGTICKSIZE, pMonthTick.y()));
274 
275         // Draw month labels
276         QRectF rMonth(mapToWidget(QPointF(0.0, float(dt.daysInYear() - dt.addMonths(-1).dayOfYear()))),
277                       mapToWidget(QPointF(dataRect().left() - 0.1, doy)));
278         QLocale locale;
279         p->drawText(rMonth, Qt::AlignRight | Qt::AlignVCenter | Qt::TextDontClip, locale.monthName(imonth - 1, QLocale::ShortFormat));
280         if (imonth == 12) // December
281         {
282             rMonth = QRectF(mapToWidget(QPointF(0.0, doy)), mapToWidget(QPointF(dataRect().left() - 0.1, 0.0)));
283             p->drawText(rMonth, Qt::AlignRight | Qt::AlignVCenter | Qt::TextDontClip, locale.monthName(imonth, QLocale::ShortFormat));
284         }
285 
286         // Draw dividers
287         if (skycal->scUI->checkBox_GridMonths->isChecked())
288         {
289             QColor c = p->pen().color();
290             c.setAlpha(100);
291             p->setPen(c);
292             p->drawLine(pMonthTick, QPointF(pixRect().right(), pMonthTick.y()));
293             c.setAlpha(255);
294             p->setPen(c);
295         }
296     }
297 
298     // Interval dividers
299     QFont origFont = p->font();
300     p->setFont(QFont("Monospace", origFont.pointSize() - 1));
301     for (KStarsDateTime kdt(QDate(y, 1, 1), QTime(12, 0, 0)); kdt.date().year() == y;
302          kdt = kdt.addDays(skycal->scUI->spinBox_Interval->value() > 7 ? skycal->scUI->spinBox_Interval->value() : 7))
303     {
304         // Draw ticks
305         float doy         = float(kdt.date().daysInYear() - kdt.date().dayOfYear());
306         QPointF pWeekTick = mapToWidget(QPointF(dataRect().right(), doy));
307         p->drawLine(pWeekTick, QPointF(pWeekTick.x() - BIGTICKSIZE, pWeekTick.y()));
308 
309         // Draw julian date
310         QRectF rJd(mapToWidget(QPointF(dataRect().right() + 0.1, doy + 2)),
311                    mapToWidget(QPointF(dataRect().right(), doy)));
312         p->drawText(rJd, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextDontClip,
313                     QString().setNum(double(kdt.djd()), 'f', 1));
314 
315         // Draw dividers
316         if (skycal->scUI->checkBox_GridWeeks->isChecked())
317         {
318             QColor c = p->pen().color();
319             c.setAlpha(50);
320             p->setPen(c);
321             p->drawLine(pWeekTick, QPointF(pixRect().left(), pWeekTick.y()));
322             c.setAlpha(255);
323             p->setPen(c);
324         }
325     }
326 
327     // Current day
328     if (skycal->scUI->checkBox_GridToday->isChecked())
329     {
330         p->setPen(QColor(Qt::yellow));
331         QDate today = QDate::currentDate();
332         float doy   = float(today.daysInYear() - today.dayOfYear());
333         p->drawLine(mapToWidget(QPointF(dataRect().left(), doy)), mapToWidget(QPointF(dataRect().right(), doy)));
334         p->drawText(mapToWidget(QPointF(dataRect().left() + 0.1, doy + 2.0)), today.toString());
335     }
336 
337     //Draw month labels along each horizon curve
338     //     if ( skycal->scUI->checkBox_LabelMonths->isChecked() ) {
339     //         p->setFont( QFont( "Monospace", origFont.pointSize() + 5 ) );
340     //         int textFlags = Qt::TextSingleLine | Qt::AlignCenter;
341     //         QFontMetricsF fm( p->font(), p->device() );
342     //
343     //         for ( int date=0; date<dateList.size(); date++ ) {
344     //             if ( dateList.at( date ).day() < 12 || dateList.at( date ).day() > 18 )
345     //                 continue;
346     //
347     //             bool noNight = false;
348     //             if ( riseTimeList.at( date ) < setTimeList.at( date ) )
349     //                 noNight = true;
350     //
351     //             int imonth = dateList.at( date ).month();
352     //
353     //             QString shortMonthName = QDate::shortMonthName( dateList.at( date ).month() );
354     //             QRectF riseLabelRect = fm.boundingRect( QRectF(0,0,1,1), textFlags, shortMonthName );
355     //             QRectF setLabelRect = fm.boundingRect( QRectF(0,0,1,1), textFlags, shortMonthName );
356     //
357     //             QDate dt( y, imonth, 15 );
358     //             float doy = float( dt.daysInYear() - dt.dayOfYear() );
359     //             float xRiseLabel, xSetLabel;
360     //             if ( noNight ) {
361     //                 xRiseLabel = 0.0;
362     //                 xSetLabel = 0.0;
363     //             } else {
364     //                 xRiseLabel = riseTimeList.at( date ) + 0.6;
365     //                 xSetLabel = setTimeList.at( date )- 0.6;
366     //             }
367     //             QPointF pRiseLabel = mapToWidget( QPointF( xRiseLabel, doy ) );
368     //             QPointF pSetLabel = mapToWidget( QPointF( xSetLabel, doy ) );
369     //
370     //             //Determine rotation angle for month labels
371     //             QDate dt1( y, imonth, 1 );
372     //             float doy1 = float( dt1.daysInYear() - dt1.dayOfYear() );
373     //             QDate dt2( y, imonth, dt1.daysInMonth() );
374     //             float doy2 = float( dt2.daysInYear() - dt2.dayOfYear() );
375     //
376     //             QPointF p1, p2;
377     //             float rAngle, sAngle;
378     //             if ( noNight ) {
379     //                 rAngle = 90.0;
380     //             } else {
381     //                 p1 = mapToWidget( QPointF( riseTimeList.at( date-2 ), doy1 ) );
382     //                 p2 = mapToWidget( QPointF( riseTimeList.at( date+2 ), doy2 ) );
383     //                 rAngle = atan2( p2.y() - p1.y(), p2.x() - p1.x() )/dms::DegToRad;
384     //
385     //                 p1 = mapToWidget( QPointF( setTimeList.at( date-2 ), doy1 ) );
386     //                 p2 = mapToWidget( QPointF( setTimeList.at( date+2 ), doy2 ) );
387     //                 sAngle = atan2( p2.y() - p1.y(), p2.x() - p1.x() )/dms::DegToRad;
388     //             }
389     //
390     //             p->save();
391     //             p->translate( pRiseLabel );
392     //             p->rotate( rAngle );
393     //             p->drawText( riseLabelRect, textFlags, shortMonthName );
394     //             p->restore();
395     //
396     //             if ( ! noNight ) {
397     //                 p->save();
398     //                 p->translate( pSetLabel );
399     //                 p->rotate( sAngle );
400     //                 p->drawText( setLabelRect, textFlags, shortMonthName );
401     //                 p->restore();
402     //             }
403     //         }
404     //     }
405 
406     p->setFont(origFont);
407 }
408