1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2012 Rene Kuettner <rene@bitkanal.net>
4 //
5 
6 #include "EclipsesItem.h"
7 
8 #include "MarbleDebug.h"
9 
10 #include <QIcon>
11 
12 namespace Marble
13 {
14 
EclipsesItem(EclSolar * ecl,int index,QObject * parent)15 EclipsesItem::EclipsesItem( EclSolar *ecl, int index, QObject *parent )
16     : QObject( parent ),
17       m_ecl( ecl ),
18       m_index( index ),
19       m_calculationsNeedUpdate( true ),
20       m_isTotal( false ),
21       m_phase( TotalSun ),
22       m_magnitude( 0. ),
23       m_centralLine(Tessellate),
24       m_umbra( Tessellate ),
25       m_southernPenumbra( Tessellate ),
26       m_northernPenumbra( Tessellate ),
27       m_shadowConeUmbra( Tessellate ),
28       m_shadowConePenumbra( Tessellate ),
29       m_shadowCone60MagPenumbra( Tessellate )
30 {
31     initialize();
32 }
33 
~EclipsesItem()34 EclipsesItem::~EclipsesItem()
35 {
36 }
37 
index() const38 int EclipsesItem::index() const
39 {
40     return m_index;
41 }
42 
takesPlaceAt(const QDateTime & dateTime) const43 bool EclipsesItem::takesPlaceAt( const QDateTime &dateTime ) const
44 {
45     return ( ( m_startDatePartial <= dateTime ) &&
46              ( m_endDatePartial >= dateTime ) );
47 }
48 
phase() const49 EclipsesItem::EclipsePhase EclipsesItem::phase() const
50 {
51     return m_phase;
52 }
53 
icon() const54 QIcon EclipsesItem::icon() const
55 {
56     switch( m_phase ) {
57         case EclipsesItem::TotalMoon:
58             return QIcon(QStringLiteral(":res/lunar_total.png"));
59         case EclipsesItem::PartialMoon:
60             return QIcon(QStringLiteral(":res/lunar_partial.png"));
61         case EclipsesItem::PenumbralMoon:
62             return QIcon(QStringLiteral(":res/lunar_penumbra.png"));
63         case EclipsesItem::PartialSun:
64             return QIcon(QStringLiteral(":res/solar_partial.png"));
65         case EclipsesItem::NonCentralAnnularSun:
66         case EclipsesItem::AnnularSun:
67             return QIcon(QStringLiteral(":res/solar_annular.png"));
68         case EclipsesItem::AnnularTotalSun:
69         case EclipsesItem::NonCentralTotalSun:
70         case EclipsesItem::TotalSun:
71             return QIcon(QStringLiteral(":res/solar_total.png"));
72     }
73 
74     return QIcon();
75 }
76 
phaseText() const77 QString EclipsesItem::phaseText() const
78 {
79     switch( m_phase ) {
80         case TotalMoon:             return tr( "Moon, Total" );
81         case PartialMoon:           return tr( "Moon, Partial" );
82         case PenumbralMoon:         return tr( "Moon, Penumbral" );
83         case PartialSun:            return tr( "Sun, Partial" );
84         case NonCentralAnnularSun:  return tr( "Sun, non-central, Annular" );
85         case NonCentralTotalSun:    return tr( "Sun, non-central, Total" );
86         case AnnularSun:            return tr( "Sun, Annular" );
87         case TotalSun:              return tr( "Sun, Total" );
88         case AnnularTotalSun:       return tr( "Sun, Annular/Total" );
89     }
90 
91     return QString();
92 }
93 
magnitude() const94 double EclipsesItem::magnitude() const
95 {
96     return m_magnitude;
97 }
98 
dateMaximum() const99 const QDateTime& EclipsesItem::dateMaximum() const
100 {
101     return m_dateMaximum;
102 }
103 
startDatePartial() const104 const QDateTime& EclipsesItem::startDatePartial() const
105 {
106     return m_startDatePartial;
107 }
108 
endDatePartial() const109 const QDateTime& EclipsesItem::endDatePartial() const
110 {
111     return m_endDatePartial;
112 }
113 
partialDurationHours() const114 int EclipsesItem::partialDurationHours() const
115 {
116     return (m_endDatePartial.toTime_t() -
117             m_startDatePartial.toTime_t()) / 3600;
118 }
119 
startDateTotal() const120 const QDateTime& EclipsesItem::startDateTotal() const
121 {
122     return m_startDateTotal;
123 }
124 
endDateTotal() const125 const QDateTime& EclipsesItem::endDateTotal() const
126 {
127     return m_endDateTotal;
128 }
129 
maxLocation()130 const GeoDataCoordinates& EclipsesItem::maxLocation()
131 {
132     if( m_calculationsNeedUpdate ) {
133         calculate();
134     }
135 
136     return m_maxLocation;
137 }
138 
centralLine()139 const GeoDataLineString& EclipsesItem::centralLine()
140 {
141     if( m_calculationsNeedUpdate ) {
142         calculate();
143     }
144 
145     return m_centralLine;
146 }
147 
umbra()148 const GeoDataLinearRing& EclipsesItem::umbra()
149 {
150     if( m_calculationsNeedUpdate ) {
151         calculate();
152     }
153 
154     return m_umbra;
155 }
156 
southernPenumbra()157 const GeoDataLineString& EclipsesItem::southernPenumbra()
158 {
159     if( m_calculationsNeedUpdate ) {
160         calculate();
161     }
162 
163     return m_southernPenumbra;
164 }
165 
northernPenumbra()166 const GeoDataLineString& EclipsesItem::northernPenumbra()
167 {
168     if( m_calculationsNeedUpdate ) {
169         calculate();
170     }
171 
172     return m_northernPenumbra;
173 }
174 
shadowConeUmbra()175 GeoDataLinearRing EclipsesItem::shadowConeUmbra()
176 {
177     if( m_calculationsNeedUpdate ) {
178         calculate();
179     }
180 
181     return m_shadowConeUmbra;
182 }
183 
shadowConePenumbra()184 GeoDataLinearRing EclipsesItem::shadowConePenumbra()
185 {
186     if( m_calculationsNeedUpdate ) {
187         calculate();
188     }
189 
190     return m_shadowConePenumbra;
191 }
192 
shadowCone60MagPenumbra()193 GeoDataLinearRing EclipsesItem::shadowCone60MagPenumbra()
194 {
195     if( m_calculationsNeedUpdate ) {
196         calculate();
197     }
198 
199     return m_shadowCone60MagPenumbra;
200 }
201 
sunBoundaries()202 const QList<GeoDataLinearRing>& EclipsesItem::sunBoundaries()
203 {
204     if( m_calculationsNeedUpdate ) {
205         calculate();
206     }
207 
208     return m_sunBoundaries;
209 }
210 
initialize()211 void EclipsesItem::initialize()
212 {
213     // set basic information
214     int year, month, day, hour, min, phase;
215     double secs, tz;
216 
217     phase = m_ecl->getEclYearInfo( m_index, year, month, day,
218                                             hour, min, secs,
219                                             tz, m_magnitude );
220 
221     switch( phase ) {
222         case -4: m_phase = EclipsesItem::TotalMoon; break;
223         case -3: m_phase = EclipsesItem::PartialMoon; break;
224         case -2:
225         case -1: m_phase = EclipsesItem::PenumbralMoon; break;
226         case  1: m_phase = EclipsesItem::PartialSun; break;
227         case  2: m_phase = EclipsesItem::NonCentralAnnularSun; break;
228         case  3: m_phase = EclipsesItem::NonCentralTotalSun; break;
229         case  4: m_phase = EclipsesItem::AnnularSun; break;
230         case  5: m_phase = EclipsesItem::TotalSun; break;
231         case  6: m_phase = EclipsesItem::AnnularTotalSun; break;
232         default:
233             mDebug() << "Invalid phase for eclipse at" << year << "/" <<
234                         day << "/" << month << "!";
235     }
236 
237     m_dateMaximum = QDateTime( QDate( year, month, day ),
238                                QTime( hour, min, secs ),
239                                Qt::LocalTime );
240 
241     // get global start/end date of eclipse
242 
243     double mjd_start, mjd_end;
244     m_ecl->putEclSelect( m_index );
245 
246     if( m_ecl->getPartial( mjd_start, mjd_end ) != 0 ) {
247         m_ecl->getDatefromMJD( mjd_start, year, month, day, hour, min, secs );
248         m_startDatePartial = QDateTime( QDate( year, month, day ),
249                                         QTime( hour, min, secs ),
250                                         Qt::LocalTime );
251         m_ecl->getDatefromMJD( mjd_end, year, month, day, hour, min, secs );
252         m_endDatePartial = QDateTime( QDate( year, month, day ),
253                                       QTime( hour, min, secs ),
254                                       Qt::LocalTime );
255     } else {
256         // duration is shorter than 1 min
257         m_startDatePartial = m_dateMaximum;
258         m_endDatePartial = m_dateMaximum;
259     }
260 
261     m_isTotal = ( m_ecl->getTotal( mjd_start, mjd_end ) != 0 );
262     if( m_isTotal ) {
263         m_ecl->getDatefromMJD( mjd_start, year, month, day, hour, min, secs );
264         m_startDateTotal = QDateTime( QDate( year, month, day ),
265                                       QTime( hour, min, secs ),
266                                       Qt::LocalTime );
267         m_ecl->getDatefromMJD( mjd_end, year, month, day, hour, min, secs );
268         m_endDateTotal = QDateTime( QDate( year, month, day ),
269                                     QTime( hour, min, secs ),
270                                     Qt::LocalTime );
271     }
272 
273     // detailed calculations are done when required
274     m_calculationsNeedUpdate = true;
275 }
276 
calculate()277 void EclipsesItem::calculate()
278 {
279     int np, kp, j;
280     double lat1, lng1, lat2, lng2, lat3, lng3, lat4, lng4;
281     double ltf[60], lnf[60];
282 
283     m_ecl->putEclSelect( m_index );
284 
285     // FIXME: set observer location
286     m_ecl->getMaxPos( lat1, lng1 );
287     m_ecl->setLocalPos( lat1, lng1, 0 );
288 
289     // eclipse's maximum location
290     m_maxLocation = GeoDataCoordinates( lng1, lat1, 0., GeoDataCoordinates::Degree );
291 
292     // calculate central line
293     np = m_ecl->eclPltCentral( true, lat1, lng1 );
294     kp = np;
295     m_centralLine.clear();
296     m_centralLine << GeoDataCoordinates( GeoDataCoordinates::normalizeLon(lng1, GeoDataCoordinates::Degree),
297                                          GeoDataCoordinates::normalizeLat(lat1, GeoDataCoordinates::Degree),
298                                          0., GeoDataCoordinates::Degree );
299 
300     if( np > 3 ) { // central eclipse
301         while( np > 3 ) {
302             np = m_ecl->eclPltCentral( false, lat1, lng1 );
303             if( np > 3 ) {
304                 m_centralLine << GeoDataCoordinates( GeoDataCoordinates::normalizeLon(lng1, GeoDataCoordinates::Degree),
305                                                      GeoDataCoordinates::normalizeLat(lat1, GeoDataCoordinates::Degree),
306                                                      0., GeoDataCoordinates::Degree );
307             }
308         }
309     }
310 
311     // calculate umbra
312     np = kp;
313     m_umbra.clear();
314     if( np > 3 ) { // total or annual eclipse
315         // northern /southern boundaries of umbra
316         np = m_ecl->centralBound( true, lat1, lng1, lat2, lng2 );
317 
318         GeoDataLinearRing lowerUmbra( Tessellate ), upperUmbra( Tessellate );
319         lowerUmbra << GeoDataCoordinates( GeoDataCoordinates::normalizeLon(lng1, GeoDataCoordinates::Degree),
320                                           GeoDataCoordinates::normalizeLat(lat1, GeoDataCoordinates::Degree),
321                                           0., GeoDataCoordinates::Degree );
322         upperUmbra << GeoDataCoordinates( GeoDataCoordinates::normalizeLon(lng1, GeoDataCoordinates::Degree),
323                                           GeoDataCoordinates::normalizeLat(lat1, GeoDataCoordinates::Degree),
324                                           0., GeoDataCoordinates::Degree );
325 
326         while( np > 0 ) {
327             np = m_ecl->centralBound( false, lat1, lng1, lat2, lng2 );
328             if( lat1 <= 90. ) {
329                 lowerUmbra << GeoDataCoordinates( GeoDataCoordinates::normalizeLon(lng1, GeoDataCoordinates::Degree),
330                                                   GeoDataCoordinates::normalizeLat(lat1, GeoDataCoordinates::Degree),
331                                                   0., GeoDataCoordinates::Degree );
332             }
333             if( lat1 <= 90. ) {
334                 upperUmbra << GeoDataCoordinates( GeoDataCoordinates::normalizeLon(lng2, GeoDataCoordinates::Degree),
335                                                   GeoDataCoordinates::normalizeLat(lat2, GeoDataCoordinates::Degree),
336                                                   0., GeoDataCoordinates::Degree );
337             }
338         }
339 
340         GeoDataLinearRing invertedUpperUmbra( Tessellate );
341         QVector<GeoDataCoordinates>::const_iterator iter = upperUmbra.constEnd() - 1;
342         for( ; iter != upperUmbra.constBegin(); --iter ) {
343             invertedUpperUmbra << *iter;
344         }
345         invertedUpperUmbra << upperUmbra.first();
346         upperUmbra = invertedUpperUmbra;
347 
348         m_umbra << lowerUmbra << upperUmbra;
349     }
350 
351     // shadow cones
352     m_shadowConeUmbra.clear();
353     m_shadowConePenumbra.clear();
354     m_shadowCone60MagPenumbra.clear();
355 
356     m_ecl->getLocalMax( lat2, lat3, lat4 );
357 
358     m_ecl->getShadowCone( lat2, true, 40, ltf, lnf );
359     for( j = 0; j < 40; ++j ) {
360         if( ltf[j] < 100. ) {
361             m_shadowConeUmbra << GeoDataCoordinates( GeoDataCoordinates::normalizeLon(lnf[j], GeoDataCoordinates::Degree),
362                                                      GeoDataCoordinates::normalizeLat(ltf[j], GeoDataCoordinates::Degree),
363                                                      0., GeoDataCoordinates::Degree );
364         }
365     }
366 
367     m_ecl->setPenumbraAngle( 1., 0 );
368     m_ecl->getShadowCone( lat2, false, 60, ltf, lnf );
369     for( j = 0; j < 60; ++j ) {
370         if( ltf[j] < 100. ) {
371             m_shadowConePenumbra << GeoDataCoordinates( GeoDataCoordinates::normalizeLon(lnf[j], GeoDataCoordinates::Degree),
372                                                         GeoDataCoordinates::normalizeLat(ltf[j], GeoDataCoordinates::Degree),
373                                                         0., GeoDataCoordinates::Degree );
374         }
375     }
376 
377     m_ecl->setPenumbraAngle( 0.6, 1 );
378     m_ecl->getShadowCone( lat2, false, 60, ltf, lnf );
379     for( j = 0; j < 60; ++j ) {
380         if( ltf[j] < 100. ) {
381             m_shadowCone60MagPenumbra << GeoDataCoordinates( GeoDataCoordinates::normalizeLon(lnf[j], GeoDataCoordinates::Degree),
382                                                              GeoDataCoordinates::normalizeLat(ltf[j], GeoDataCoordinates::Degree),
383                                                              0., GeoDataCoordinates::Degree );
384         }
385     }
386 
387     m_ecl->setPenumbraAngle( 1., 0 );
388 
389     // eclipse boundaries
390     m_southernPenumbra.clear();
391     m_northernPenumbra.clear();
392 
393     np = m_ecl->GNSBound( true, true, lat1, lng2 );
394     while( np > 0 ) {
395         np = m_ecl->GNSBound( false, true, lat1, lng1 );
396         if( ( np > 0 ) && ( lat1 <= 90. ) ) {
397             m_southernPenumbra << GeoDataCoordinates( GeoDataCoordinates::normalizeLon(lng1, GeoDataCoordinates::Degree),
398                                                       GeoDataCoordinates::normalizeLat(lat1, GeoDataCoordinates::Degree),
399                                                       0., GeoDataCoordinates::Degree );
400         }
401     }
402 
403     np = m_ecl->GNSBound( true, false, lat1, lng1 );
404     while( np > 0 ) {
405         np = m_ecl->GNSBound( false, false, lat1, lng1 );
406         if( ( np > 0 ) && ( lat1 <= 90. ) ) {
407             m_northernPenumbra << GeoDataCoordinates( GeoDataCoordinates::normalizeLon(lng1, GeoDataCoordinates::Degree),
408                                                       GeoDataCoordinates::normalizeLat(lat1, GeoDataCoordinates::Degree),
409                                                       0., GeoDataCoordinates::Degree );
410         }
411     }
412 
413     // sunrise / sunset boundaries
414 
415     QList<GeoDataLinearRing*> sunBoundaries;
416     np = m_ecl->GRSBound( true, lat1, lng1, lat3, lng3 );
417 
418     GeoDataLinearRing *lowerBoundary = new GeoDataLinearRing( Tessellate );
419     *lowerBoundary << GeoDataCoordinates( GeoDataCoordinates::normalizeLon(lng1, GeoDataCoordinates::Degree),
420                                           GeoDataCoordinates::normalizeLat(lat1, GeoDataCoordinates::Degree),
421                                           0., GeoDataCoordinates::Degree );
422 
423     GeoDataLinearRing *upperBoundary = new GeoDataLinearRing( Tessellate );
424     *upperBoundary << GeoDataCoordinates( GeoDataCoordinates::normalizeLon(lng3, GeoDataCoordinates::Degree),
425                                           GeoDataCoordinates::normalizeLat(lat3, GeoDataCoordinates::Degree),
426                                           0., GeoDataCoordinates::Degree );
427 
428     m_sunBoundaries.clear();
429 
430     while ( np > 0 ) {
431         np = m_ecl->GRSBound( false, lat2, lng2, lat4, lng4 );
432         bool pline = fabs( lng1 - lng2 ) < 10.; // during partial eclipses, the Rise/Set
433                                                 // lines switch at one stage.
434                                                 // This will prevent an ugly line between
435                                                 // the switch points. If there is a
436                                                 // longitude jump then add the current
437                                                 // section to our sun boundaries collection
438                                                 // and start a new section
439         if ( !pline && !lowerBoundary->isEmpty() ) {
440             sunBoundaries.prepend( lowerBoundary );
441             lowerBoundary = new GeoDataLinearRing( Tessellate );
442         }
443         if ( ( np > 0 ) && ( lat2 <= 90. ) && ( lat1 <= 90. ) ) {
444             *lowerBoundary << GeoDataCoordinates( GeoDataCoordinates::normalizeLon(lng2, GeoDataCoordinates::Degree),
445                                                   GeoDataCoordinates::normalizeLat(lat2, GeoDataCoordinates::Degree),
446                                                   0., GeoDataCoordinates::Degree );
447         }
448         pline = fabs( lng3 - lng4 ) < 10.; // during partial eclipses, the Rise/Set lines
449                                            // switch at one stage.
450                                            // This will prevent an ugly line between the
451                                            // switch points. If there is a longitude jump
452                                            // then add the current section to our sun
453                                            // boundaries collection and start a new section
454         if ( !pline && !upperBoundary->isEmpty() ) {
455             sunBoundaries.prepend( upperBoundary );
456             upperBoundary = new GeoDataLinearRing( Tessellate );
457         }
458         if ( pline && ( np > 0 ) && ( lat4 <= 90. ) && ( lat3 <= 90. ) ) {
459             *upperBoundary << GeoDataCoordinates( GeoDataCoordinates::normalizeLon(lng4, GeoDataCoordinates::Degree),
460                                                   GeoDataCoordinates::normalizeLat(lat4, GeoDataCoordinates::Degree),
461                                                   0., GeoDataCoordinates::Degree );
462         }
463 
464         lng1 = lng2;
465         lat1 = lat2;
466         lng3 = lng4;
467         lat3 = lat4;
468     }
469 
470     if ( !lowerBoundary->isEmpty() ) {
471         sunBoundaries.prepend(lowerBoundary);
472     } else {
473         delete lowerBoundary;
474     }
475     if ( !upperBoundary->isEmpty() ) {
476         sunBoundaries.prepend(upperBoundary);
477     } else {
478         delete upperBoundary;
479     }
480 
481     for ( int result = 0; result < 2; ++result ) {
482         GeoDataLinearRing sunBoundary( Tessellate );
483 
484         sunBoundary = *sunBoundaries.last();
485         sunBoundaries.pop_back();
486 
487         while ( sunBoundaries.size() > 0) {
488             int closestSection = -1;
489 
490             // TODO: Now that MableMath is not public anymore we need a
491             // GeoDataCoordinates::distance() method in Marble.
492             GeoDataLineString ruler;
493             ruler << sunBoundary.last() << sunBoundary.first();
494             qreal closestDistance = ruler.length( 1 );
495             int closestEnd = 0;  // 0 = start of section, 1 = end of section
496 
497             // Look for a section that is closest to our sunBoundary section.
498             for ( int it = 0; it < sunBoundaries.size(); ++it ) {
499                 GeoDataLineString distanceStartSection;
500                 distanceStartSection << sunBoundary.last() << sunBoundaries.at( it )->first();
501 
502                 GeoDataLineString distanceEndSection;
503                 distanceEndSection << sunBoundary.last() << sunBoundaries.at( it )->last();
504                 if ( distanceStartSection.length( 1 ) < closestDistance ) {
505                     closestDistance = distanceStartSection.length( 1 );
506                     closestSection = it;
507                     closestEnd = 0;
508                 }
509                 if ( distanceEndSection.length(1) < closestDistance ) {
510                     closestDistance = distanceEndSection.length( 1 );
511                     closestSection = it;
512                     closestEnd = 1;
513                 }
514             }
515 
516             if ( closestSection == -1 ) {
517                 // There is no other section that is closer to the end of
518                 // our sunBoundary section than the startpoint of our
519                 // sunBoundary itself
520                 break;
521             }
522             else {
523                 // We now concatenate the closest section to the sunBoundary.
524                 // First we might have to invert it so that we concatenate
525                 // the right end
526                 if ( closestEnd == 1 ) {
527                     // TODO: replace this with a GeoDataLinearRing::invert()
528                     // method that needs to be added to Marble ...
529                     GeoDataLinearRing * invertedBoundary = new GeoDataLinearRing( Tessellate );
530                     QVector<GeoDataCoordinates>::const_iterator iter = sunBoundaries.at( closestSection )->constEnd();
531                     --iter;
532                     for( ; iter != sunBoundaries.at( closestSection )->constBegin(); --iter ) {
533                         *invertedBoundary << *iter;
534                     }
535                     *invertedBoundary << sunBoundaries.at( closestSection )->first();
536                     delete sunBoundaries[closestSection];
537                     sunBoundaries[closestSection] = invertedBoundary;
538                 }
539                 sunBoundary << *sunBoundaries[closestSection];
540 
541                 // Now remove the section that we've just added from the list
542                 delete sunBoundaries[closestSection];
543                 sunBoundaries.removeAt( closestSection );
544             }
545         }
546 
547         m_sunBoundaries << sunBoundary;
548 
549         if ( sunBoundaries.size() == 0 ) break;
550     }
551 
552     m_calculationsNeedUpdate = false;
553 }
554 
555 } // Namespace Marble
556 
557 #include "moc_EclipsesItem.cpp"
558 
559