• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Stellarium / stellarium / 15291801018

28 May 2025 04:52AM UTC coverage: 11.931% (-0.02%) from 11.951%
15291801018

push

github

alex-w
Added new set of navigational stars (XIX century)

0 of 6 new or added lines in 2 files covered. (0.0%)

14124 existing lines in 74 files now uncovered.

14635 of 122664 relevant lines covered (11.93%)

18291.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/core/SolarEclipseComputer.cpp
1
/*
2
 * Stellarium
3
 * Copyright (C) 2022 Worachate Boonplod
4
 * Copyright (C) 2022 Georg Zotti
5
 * Copyright (C) 2024 Ruslan Kabatsayev
6
 *
7
 * This program is free software; you can redistribute it and/or
8
 * modify it under the terms of the GNU General Public License
9
 * as published by the Free Software Foundation; either version 2
10
 * of the License, or (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU General Public License for more details.
16
 * You should have received a copy of the GNU General Public License
17
 * along with this program; if not, write to the Free Software
18
 * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA  02110-1335, USA.
19
*/
20

21
#include "SolarEclipseComputer.hpp"
22
#include "StelLocaleMgr.hpp"
23
#include "StelModuleMgr.hpp"
24
#include "SolarSystem.hpp"
25
#include "StelUtils.hpp"
26
#include "StelCore.hpp"
27
#include "sidereal_time.h"
28

29
#include <QGlobalStatic>
30
#include <utility>
31
#include <QPainter>
32

33
namespace
34
{
35

UNCOV
36
double sqr(const double x) { return x*x; }
×
37

UNCOV
38
Q_GLOBAL_STATIC_WITH_ARGS(QColor, hybridEclipseColor ,(128, 0, 128));
×
UNCOV
39
Q_GLOBAL_STATIC_WITH_ARGS(QColor, totalEclipseColor  ,(255, 0, 0));
×
UNCOV
40
Q_GLOBAL_STATIC_WITH_ARGS(QColor, annularEclipseColor,(0, 0, 255));
×
UNCOV
41
Q_GLOBAL_STATIC_WITH_ARGS(QColor, eclipseLimitsColor ,(0, 255, 0));
×
42

UNCOV
43
QString toKMLColorString(const QColor& c)
×
44
{
45
        return QString("%1%2%3%4").arg(c.alpha(),2,16,QChar('0'))
×
46
                                  .arg(c.blue (),2,16,QChar('0'))
×
47
                                  .arg(c.green(),2,16,QChar('0'))
×
UNCOV
48
                                  .arg(c.red  (),2,16,QChar('0'));
×
49
}
50

UNCOV
51
void drawContinuousEquirectGeoLine(QPainter& painter, QPointF pointA, QPointF pointB)
×
52
{
53
        using namespace std;
UNCOV
54
        const auto lineLengthDeg = hypot(pointB.x() - pointA.x(), pointB.y() - pointA.y());
×
55
        // For short enough lines go the simple way
UNCOV
56
        if(lineLengthDeg < 2)
×
57
        {
58
                painter.drawLine(pointA, pointB);
×
UNCOV
59
                return;
×
60
        }
61

62
        // Order them west to east
63
        if(pointA.x() > pointB.x())
×
UNCOV
64
                swap(pointA, pointB);
×
65

66
        Vec3d dirA;
×
UNCOV
67
        StelUtils::spheToRect(M_PI/180 * pointA.x(), M_PI/180 * pointA.y(), dirA);
×
68

69
        Vec3d dirB;
×
UNCOV
70
        StelUtils::spheToRect(M_PI/180 * pointB.x(), M_PI/180 * pointB.y(), dirB);
×
71

72
        const auto cosAngleBetweenDirs = dot(dirA, dirB);
×
UNCOV
73
        const auto angleMax = acos(cosAngleBetweenDirs);
×
74

75
        // Create an orthonormal pair of vectors that will define a plane in which we'll
76
        // rotate from one of the vectors towards the second one, thus creating the
77
        // shortest line on a unit sphere between the previous and the current directions.
78
        const auto firstDir = dirA;
×
UNCOV
79
        const auto secondDir = normalize(dirB - cosAngleBetweenDirs*firstDir);
×
80

UNCOV
81
        auto prevPoint = firstDir;
×
82
        // Keep the step no greater than 2°
83
        const double numPoints = max(3., ceil(lineLengthDeg / 2.));
×
UNCOV
84
        for(int n = 1; n < numPoints; ++n)
×
85
        {
86
                const double alpha = double(n) / (numPoints-1) * angleMax;
×
UNCOV
87
                const Vec3d currPoint = cos(alpha)*firstDir + sin(alpha)*secondDir;
×
88

89
                double lon1, lat1;
UNCOV
90
                StelUtils::rectToSphe(&lon1, &lat1, prevPoint);
×
91

92
                double lon2, lat2;
UNCOV
93
                StelUtils::rectToSphe(&lon2, &lat2, currPoint);
×
94

95
                // If current point happens to have wrapped around 180°, bring it back to
96
                // the eastern side (the check relies on the ordering of the endpoints).
97
                if(pointA.x() > 0 && lon2 < 0)
×
UNCOV
98
                        lon2 += 2*M_PI;
×
99

100
                painter.drawLine(180/M_PI * QPointF(lon1,lat1), 180/M_PI * QPointF(lon2,lat2));
×
UNCOV
101
                prevPoint = currPoint;
×
102
        }
103
}
104

UNCOV
105
void drawGeoLinesForEquirectMap(QPainter& painter, const std::vector<QPointF>& points)
×
106
{
UNCOV
107
        if(points.empty()) return;
×
108

109
        using namespace std;
110

111
        Vec3d prevDir;
×
112
        StelUtils::spheToRect(M_PI/180 * points[0].x(), M_PI/180 * points[0].y(), prevDir);
×
UNCOV
113
        for(unsigned n = 1; n < points.size(); ++n)
×
114
        {
115
                const double currLon = M_PI/180 * points[n].x();
×
116
                const double currLat = M_PI/180 * points[n].y();
×
117
                Vec3d currDir;
×
UNCOV
118
                StelUtils::spheToRect(currLon, currLat, currDir);
×
119

UNCOV
120
                const double cosAngleBetweenDirs = dot(prevDir, currDir);
×
121
                // Create an orthonormal pair of vectors that will define a plane in which we'll
122
                // rotate from one of the vectors towards the second one, thus creating the
123
                // shortest line on a unit sphere between the previous and the current directions.
124
                const Vec3d firstDir = prevDir;
×
UNCOV
125
                const Vec3d secondDir = normalize(currDir - cosAngleBetweenDirs*firstDir);
×
126
                // The parametric equation for the connecting line is:
127
                //  P(alpha) = cos(alpha)*firstDir+sin(alpha)*secondDir.
128
                // Here we assume alpha>0 (otherwise we'll go the longer route over the sphere).
129
                //
130
                // Now we need to find out if this line crosses the 180° meridian. This happens
131
                // if there exists an alpha such that P(alpha).y==0 && P(alpha).x<0.
132
                // These are the solutions of this equation for alpha.
133
                const double alpha1 = atan2(firstDir[1], -secondDir[1]);
×
134
                const double alpha2 = atan2(-firstDir[1], secondDir[1]);
×
135
                const bool firstSolutionBad  = alpha1 < 0 || cos(alpha1) < cosAngleBetweenDirs;
×
UNCOV
136
                const bool secondSolutionBad = alpha2 < 0 || cos(alpha2) < cosAngleBetweenDirs;
×
137
                // If the line doesn't cross 180°, we are not splitting it
UNCOV
138
                if(firstSolutionBad && secondSolutionBad)
×
139
                {
140
                        drawContinuousEquirectGeoLine(painter, points[n-1], points[n]);
×
141
                        prevDir = currDir;
×
UNCOV
142
                        continue;
×
143
                }
144

145
                const double alpha = firstSolutionBad ? alpha2 : alpha1;
×
UNCOV
146
                const Vec3d P = cos(alpha)*firstDir + sin(alpha)*secondDir;
×
147
                // Ignore the crossing of 0°
UNCOV
148
                if(P[0] > 0)
×
149
                {
150
                        drawContinuousEquirectGeoLine(painter, points[n-1], points[n]);
×
151
                        prevDir = currDir;
×
UNCOV
152
                        continue;
×
153
                }
154

155
                // So, we've found the crossing. Let's split our line by the crossing point.
156
                double crossLon, crossLat;
157
                StelUtils::rectToSphe(&crossLon, &crossLat, P);
×
158
                crossLon *= 180/M_PI;
×
159
                crossLat *= 180/M_PI;
×
160
                const bool sameSign = (crossLon < 0 && points[n-1].x() < 0) || (crossLon >= 0 && points[n-1].x() >= 0);
×
161
                drawContinuousEquirectGeoLine(painter, points[n-1], QPointF(sameSign ? crossLon : -crossLon, crossLat));
×
UNCOV
162
                drawContinuousEquirectGeoLine(painter, points[n], QPointF(sameSign ? -crossLon : crossLon, crossLat));
×
163

UNCOV
164
                prevDir = currDir;
×
165
        }
166
}
167

UNCOV
168
double zetaFromQ(const double cosQ, const double sinQ, const double tf, const EclipseBesselParameters& bp)
×
169
{
170
        // Reference: Explanatory Supplement to the Astronomical Ephemeris
171
        // and the American Ephemeris and Nautical Almanac, 3rd Edition (2013)
172

173
        const double x = bp.elems.x, y = bp.elems.y, d = bp.elems.d,
×
174
                     mudot = bp.mudot, bdot = bp.bdot, cdot = bp.cdot, ddot = bp.ddot;
×
175
        const double cosd = cos(d);
×
UNCOV
176
        const double adot = -bp.ldot-mudot*x*tf*cosd + y*ddot*tf;
×
177

178
        // From equation (11.81), restoring the missing dots over a,b,c in the book (cf. eq. (11.78)).
UNCOV
179
        return (-adot + bdot * cosQ - cdot * sinQ) / ((1 + sqr(tf)) * (ddot * cosQ - mudot * cosd * sinQ));
×
180
}
181

182
struct ShadowLimitPoints
183
{
184
        EclipseBesselParameters bp;
185
        double JD;
186
        struct Point
187
        {
188
                double Q;
189
                double zeta;
190
        };
191
        QVarLengthArray<Point, 8> values;
192

193
        ShadowLimitPoints(const EclipseBesselParameters& bp, const double JD)
×
194
                : bp(bp)
×
UNCOV
195
                , JD(JD)
×
196
        {
UNCOV
197
        }
×
198
};
199

UNCOV
200
ShadowLimitPoints getShadowLimitQs(StelCore*const core, const double JD, const double e2, const bool penumbra)
×
201
{
202
        /*
203
         * Reference: Explanatory Supplement to the Astronomical Ephemeris and the American Ephemeris and Nautical Almanac,
204
         * 3rd Edition (2013).
205
         *
206
         * This function computes Q values for shadow limit points. The equation being solved here is obtained from the
207
         * main identity (11.56):
208
         *
209
         *                                            xi^2+eta1^2+zeta1^2=1,
210
         *
211
         * and (11.60):
212
         *
213
         *                                  zeta=rho2*[zeta1*cos(d1-d2)-eta1*sin(d1-d2)].
214
         *
215
         * Solve the latter equation for zeta1 and substitute the result into the former equation.  Then multiply both
216
         * sides of the result by rho1^2*rho2^2*cos(d1-d2)^2, and after a rearrangement you'll get:
217
         *
218
         *   zeta^2*rho1^2 + 2*zeta*eta*sin(d1-d2)*rho1*rho2 +
219
         *                              + eta^2*rho2^2 - cos(d1-d2)^2*rho1^2*rho2^2 + xi^2*cos(d1-d2)^2*rho1^2*rho2^2 = 0.
220
         *
221
         * Now substitute xi and eta with (eq. (11.82))
222
         *
223
         *                                              xi = x - L*sin(Q),
224
         *                                             eta = x - L*cos(Q),
225
         *
226
         * where (eq. (11.65))
227
         *
228
         *                                             L = l - zeta*tan(f).
229
         *
230
         * The result will be a bit more complicated:
231
         *
232
         *  zeta^2*rho1^2 - cos(d1-d2)^2*rho1^2*rho2^2 + 2*zeta*sin(d1-d2)*rho1*rho2*(y - cos(Q)*(l - zeta*tan(f))) +
233
         *    + rho2^2*(y - cos(Q)*(l - zeta*tan(f)))^2 + cos(d1-d2)^2*rho1^2*rho2^2*(x - sin(Q)*(l - zeta*tan(f)))^2 = 0.
234
         *
235
         * Now replace zeta by the expression obtainable from (11.78),
236
         *
237
         *                                              -adot + bdot*cos(Q) - cdot*sin(Q)
238
         *                               zeta = --------------------------------------------------,
239
         *                                      (1 + tan(f)^2)*(ddot*cos(Q) - mudot*cos(d)*sin(Q))
240
         *
241
         * yielding, after multiplying both sides of the result by the square of the denominator of the above expression,
242
         * the final equation:
243
         *
244
         * (-adot + bdot*cos(Q) - cdot*sin(Q))^2*(rho1^2 + 2*cos(Q)*sin(d1 - d2)*rho1*rho2*tan(f) +
245
         *  + (cos(Q)^2 + cos(d1 - d2)^2*sin(Q)^2*rho1^2)*rho2^2*tan(f)^2) +
246
         *  + 2*(-adot + bdot*cos(Q) - cdot*sin(Q))*rho2*(sin(d1 - d2)*(y - cos(Q)*l)*rho1 +
247
         *    + cos(Q)*(y - cos(Q)*l)*rho2*tan(f) + cos(d1 - d2)^2*sin(Q)*(x - sin(Q)*l)*rho1^2*rho2*tan(f)) *
248
         *       * (1 + tan(f)^2)*(cos(Q)*ddot - cos(d)*sin(Q)*mudot) +
249
         *    + ((y - cos(Q)*l)^2 + cos(d1 - d2)^2*(-1 + x - sin(Q)*l)*(1 + x - sin(Q)*l)*rho1^2)*rho2^2 *
250
         *       * (1 + tan(f)^2)^2*(cos(Q)*ddot - cos(d)*sin(Q)*mudot)^2 = 0.
251
         *
252
         * As we are using the Newton's method to solve it, we need to separate the different powers of sin(Q)^n*cos(Q)^m
253
         * to be able to find a derivative. This will then yield the final result implemented in the code below.
254
         */
255

256
        core->setJD(JD);
×
257
        core->update(0);
×
258
        const auto bp = calcBesselParameters(penumbra);
×
259
        const double x = bp.elems.x, y = bp.elems.y, d = bp.elems.d, tf1 = bp.elems.tf1,
×
260
                     bdot = bp.bdot, cdot = bp.cdot, ddot = bp.ddot, mudot = bp.mudot,
×
261
                     tf2 = bp.elems.tf2, L1 = bp.elems.L1, L2 = bp.elems.L2;
×
262
        const double tf = penumbra ? tf1 : tf2;
×
263
        const double L = penumbra ? L1 : L2;
×
UNCOV
264
        const double adot = -bp.ldot-mudot*x*tf*std::cos(d) + y*ddot*tf;
×
265

266
        using namespace std;
267
        const double sind = sin(d);
×
268
        const double cosd = cos(d);
×
269
        const double rho1 = sqrt(1-e2*sqr(cosd));
×
270
        const double rho2 = sqrt(1-e2*sqr(sind));
×
271
        const double sdd = e2*sind*cosd/(rho1*rho2);   // sin(d1-d2)
×
UNCOV
272
        const double cdd = sqrt(1-sqr(sdd));           // cos(d1-d2)
×
273

274
        // Some convenience variables to shorten the expressions below
275
        const double tfSp1 = 1 + sqr(tf);
×
276
        const double tfSp12 = sqr(tfSp1);
×
277
        const double x2 = x*x;
×
278
        const double y2 = y*y;
×
279
        const double adot2 = sqr(adot);
×
280
        const double bdot2 = sqr(bdot);
×
281
        const double cdot2 = sqr(cdot);
×
282
        const double rho12 = sqr(rho1);
×
283
        const double rho22 = sqr(rho2);
×
UNCOV
284
        const double cdd2 = sqr(cdd);
×
285

286
        // Coefficients of the LHS of the equation we are going to solve.
287

288
        //  * Constant term
UNCOV
289
        const double cC0S0 = adot2 * rho12;
×
290

291
        //  * coefficient for cos(Q)
UNCOV
292
        const double cC1S0 = 2*adot*rho1*(-(bdot*rho1) + rho2*sdd*(adot*tf - ddot*tfSp1*y));
×
293
        //  * coefficient for cos(Q)^2
294
        const double cC2S0 = bdot2*rho12 + 2*bdot*rho1*rho2*sdd*(-2*adot*tf + ddot*tfSp1*y) +
×
295
                             rho2*(2*adot*ddot*tfSp1*(L*rho1*sdd - rho2*tf*y) + adot2*rho2*sqr(tf) +
×
296
                                   cdd2*rho12*rho2*tfSp12*sqr(ddot)*(-1 + x2) +
×
UNCOV
297
                                   rho2*tfSp12*sqr(ddot)*y2);
×
298
        //  * coefficient for cos(Q)^3
299
        const double cC3S0 = -2*rho2*(-(bdot*tf) + ddot*L*tfSp1)*(bdot*rho1*sdd - adot*rho2*tf +
×
UNCOV
300
                                                                  ddot*rho2*tfSp1*y);
×
301
        //  * coefficient for cos(Q)^4
UNCOV
302
        const double cC4S0 = rho22*sqr(bdot*tf - ddot*L*tfSp1);
×
303

304
        //  * coefficient for sin(Q)
UNCOV
305
        const double cC0S1 = 2*adot*rho1*(cdot*rho1 + cosd*mudot*rho2*sdd*tfSp1*y);
×
306
        //  * coefficient for sin(Q)^2
307
        const double cC0S2 = cdot2*rho12 + 2*cdot*cosd*mudot*rho1*rho2*sdd*tfSp1*y +
×
308
                             rho22*(cdd2*rho12*(adot*tf + cosd*mudot*tfSp1*(-1 + x)) *
×
309
                                    (adot*tf + cosd*mudot*tfSp1*(1 + x)) +
×
UNCOV
310
                                    tfSp12*sqr(cosd)*sqr(mudot)*y2);
×
311
        //  * coefficient for sin(Q)^3
312
        const double cC0S3 = -2*cdd2*rho12*rho22*(-(cdot*tf) + cosd*L*mudot*tfSp1) *
×
UNCOV
313
                             (adot*tf + cosd*mudot*tfSp1*x);
×
314
        //  * coefficient for sin(Q)^4
UNCOV
315
        const double cC0S4 = cdd2*rho12*rho22*sqr(cdot*tf - cosd*L*mudot*tfSp1);
×
316

317
        //  * coefficient for cos(Q)*sin(Q)
318
        const double cC1S1 = -2*bdot*rho1*(cdot*rho1 + cosd*mudot*rho2*sdd*tfSp1*y) -
×
319
                             2*rho2*(ddot*tfSp1*y*(cdot*rho1*sdd + cosd*mudot*rho2*tfSp1*y) +
×
320
                                     adot*(-2*cdot*rho1*sdd*tf + cosd*mudot*tfSp1*(L*rho1*sdd - rho2*tf*y)) +
×
UNCOV
321
                                     cdd2*ddot*rho12*rho2*tfSp1*(adot*tf*x + cosd*mudot*tfSp1*(-1 + x2)));
×
322

323
        //  * coefficient for cos(Q)^2*sin(Q)^2
324
        const double cC2S2 = rho22*(-2*cdot*cosd*L*mudot*tf*tfSp1 + tfSp12*sqr(cosd)*sqr(L)*sqr(mudot) +
×
UNCOV
325
                                    cdot2*sqr(tf) + cdd2*rho12*sqr(bdot*tf - ddot*L*tfSp1));
×
326

327
        //  * coefficient for cos(Q)*sin(Q)^2
328
        const double cC1S2 = 2*rho2*(cdot2*rho1*sdd*tf + adot*cdd2*rho12*rho2*tf*(-(bdot*tf) + ddot*L*tfSp1) +
×
329
                                     cdot*tfSp1*(-(rho1*(cosd*L*mudot*sdd + cdd2*ddot*rho1*rho2*tf*x)) +
×
330
                                                 cosd*mudot*rho2*tf*y) + cosd*mudot*rho2*tfSp1*
×
UNCOV
331
                                     (cdd2*rho12*(-(bdot*tf) + 2*ddot*L*tfSp1)*x - cosd*L*mudot*tfSp1*y));
×
332
        //  * coefficient for cos(Q)^2*sin(Q)
333
        const double cC2S1 = 2*rho2*(tfSp1*(-(adot*cosd*L*mudot*rho2*tf) + bdot*cdd2*ddot*rho12*rho2*tf*x +
×
334
                                            ddot*L*rho2*tfSp1*(-(cdd2*ddot*rho12*x) + 2*cosd*mudot*y) +
×
335
                                            bdot*cosd*mudot*(L*rho1*sdd - rho2*tf*y)) +
×
336
                                     cdot*(tf*(-2*bdot*rho1*sdd + adot*rho2*tf) +
×
UNCOV
337
                                           ddot*tfSp1*(L*rho1*sdd - rho2*tf*y)));
×
338

339
        //  * coefficient for cos(Q)^3*sin(Q)
UNCOV
340
        const double cC3S1 = -2*rho22*(-(bdot*tf) + ddot*L*tfSp1)*(-(cdot*tf) + cosd*L*mudot*tfSp1);
×
341
        //  * coefficient for cos(Q)*sin(Q)^3
UNCOV
342
        const double cC1S3 = -2*cdd2*rho12*rho22*(-(bdot*tf) + ddot*L*tfSp1)*(-(cdot*tf) + cosd*L*mudot*tfSp1);
×
343

344
        const double lhsScale = max({abs(cC0S0),
×
345
                                     abs(cC1S0),
×
346
                                     abs(cC2S0),
×
347
                                     abs(cC3S0),
×
348
                                     abs(cC4S0),
×
349
                                     abs(cC0S1),
×
350
                                     abs(cC0S2),
×
351
                                     abs(cC0S3),
×
352
                                     abs(cC0S4),
×
353
                                     abs(cC1S1),
×
354
                                     abs(cC2S2),
×
355
                                     abs(cC1S2),
×
356
                                     abs(cC2S1),
×
357
                                     abs(cC3S1),
×
UNCOV
358
                                     abs(cC1S3)});
×
359

360
        // LHS(Q) of the equation and its derivative
UNCOV
361
        const auto lhsAndDerivative = [=](const double Q) -> std::pair<double,double>
×
362
        {
363
                const double sinQ = std::sin(Q);
×
364
                const double cosQ = std::cos(Q);
×
365
                const double sinQ_2 = sqr(sinQ);
×
366
                const double cosQ_2 = sqr(cosQ);
×
367
                const double sinQ_3 = sinQ_2 * sinQ;
×
368
                const double cosQ_3 = cosQ_2 * cosQ;
×
369
                const double sinQ_4 = sqr(sinQ_2);
×
370
                const double cosQ_4 = sqr(cosQ_2);
×
371
                const double lhs = cC0S0 +
×
372
                                 cC1S0*cosQ + cC2S0*cosQ_2 + cC3S0*cosQ_3 + cC4S0*cosQ_4 +
×
373
                                 cC0S1*sinQ + cC0S2*sinQ_2 + cC0S3*sinQ_3 + cC0S4*sinQ_4 +
×
374
                                 cC1S1*cosQ*sinQ + cC1S2*cosQ*sinQ_2 + cC1S3*cosQ*sinQ_3 +
×
375
                                 cC2S1*cosQ_2*sinQ + cC2S2*cosQ_2*sinQ_2 +
×
376
                                 cC3S1*cosQ_3*sinQ;
×
377
                const double lhsPrime =
×
378
                    -cC1S0*sinQ -2*cC2S0*cosQ*sinQ - 3*cC3S0*cosQ_2*sinQ - 4*cC4S0*cosQ_3*sinQ +
×
379
                     cC0S1*cosQ + 2*cC0S2*sinQ*cosQ + 3*cC0S3*sinQ_2*cosQ + 4*cC0S4*sinQ_3*cosQ +
×
380
                     cC1S1*(cosQ_2-sinQ_2) + cC1S2*(2*cosQ_2*sinQ-sinQ_3) + cC1S3*(3*cosQ_2*sinQ_2-sinQ_4) +
×
381
                     cC2S1*(cosQ_3-2*cosQ*sinQ_2) + cC2S2*(2*cosQ_3*sinQ-2*cosQ*sinQ_3) +
×
382
                     cC3S1*(cosQ_4-3*cosQ_2*sinQ_2);
×
383
                return std::make_pair(lhs, lhsPrime);
×
UNCOV
384
        };
×
385

386
        // Find roots using Newton's method. The LHS of the equation is divided
387
        // by sin((Q-rootQ)/2) for all known roots to find subsequent roots.
388
        ShadowLimitPoints points(bp, JD);
×
389
        double Q = 0;
×
390
        bool rootFound = false;
×
UNCOV
391
        do
×
392
        {
393
                rootFound = false;
×
UNCOV
394
                bool finalIteration = false;
×
395
                // Max retry count is chosen with the idea that we'll scan the (periodic) domain [-pi,pi] with 4
396
                // extrema approximately evenly distributed over the domain, aiming to hit each slope, with an extra
397
                // sample just to make sure that we've not missed anything.
398
                // If we don't do these retries, we may lose roots when Newton's method gets stuck at an extremum that
399
                // doesn't reach zero.
400
                const double maxRetries = 9;
×
UNCOV
401
                for(double retryCount = 0; retryCount < maxRetries && !rootFound; ++retryCount)
×
402
                {
UNCOV
403
                        Q = 2*M_PI*retryCount/maxRetries;
×
404

UNCOV
405
                        for(int n = 0; n < 50; ++n)
×
406
                        {
UNCOV
407
                                const auto [lhs, lhsPrime] = lhsAndDerivative(Q);
×
408

409
                                // Cancel the known roots to avoid finding them instead of the remaining ones
410
                                double newLHSPrime = lhsPrime;
×
411
                                double newLHS = lhs;
×
UNCOV
412
                                for(const auto root : points.values)
×
413
                                {
UNCOV
414
                                        const double rootQ = root.Q;
×
415
                                        // We need a 2pi-periodic root-canceling function,
416
                                        // so take a sine of half the difference.
417
                                        const double sinDiff = sin((Q - rootQ)/2);
×
UNCOV
418
                                        const double cosDiff = cos((Q - rootQ)/2);
×
419

420
                                        newLHS /= sinDiff;
×
UNCOV
421
                                        newLHSPrime = (newLHSPrime - 0.5*cosDiff*newLHS)/sinDiff;
×
422
                                }
UNCOV
423
                                if(!isfinite(newLHS) || !isfinite(newLHSPrime))
×
424
                                {
425
                                        qWarning().nospace() << "Hit infinite/NaN values: LHS = " << newLHS
×
426
                                                             << ", LHS'(Q) = " << newLHSPrime << " at Q = " << Q;
×
UNCOV
427
                                        break;
×
428
                                }
429

430
                                if(abs(newLHS) < 1e-10*lhsScale)
×
UNCOV
431
                                        finalIteration = true;
×
432

433
                                const double deltaQ = newLHS / newLHSPrime;
×
UNCOV
434
                                if(newLHSPrime==0 || abs(deltaQ) > 1000)
×
435
                                {
436
                                        // We are shooting too far away, convergence may be too slow.
437
                                        // Let's try perturbing Q and retrying.
438
                                        Q += 0.01;
×
439
                                        finalIteration = false;
×
UNCOV
440
                                        continue;
×
441
                                }
442
                                Q -= deltaQ;
×
443
                                Q = StelUtils::fmodpos(Q, 2*M_PI);
×
UNCOV
444
                                if(Q > M_PI/2)
×
445
                                {
446
                                        // We want to avoid jumps at 0 or 2pi, because these values are likely to be
447
                                        // crossed during the course of an eclipse, unlike the values of +pi or -pi.
448
                                        // This jump would mess up our sorting by Q, leading to connecting lines from
449
                                        // the north border to the south one.  So we make sure that the discontinuity
450
                                        // is at ±pi instead of 2pi.
UNCOV
451
                                        Q -= 2*M_PI;
×
452
                                }
453

UNCOV
454
                                if(finalIteration)
×
455
                                {
UNCOV
456
                                        points.values.push_back({Q, zetaFromQ(cos(Q),sin(Q),tf,bp)});
×
457
                                        // Set a new initial value, but try to avoid setting it to 0 if the current
458
                                        // root is close to it (all values here are arbitrary in other respects).
UNCOV
459
                                        Q = abs(Q) > 0.5 ? 0 : -M_PI/2;
×
460

461
                                        rootFound = true;
×
UNCOV
462
                                        break;
×
463
                                }
464
                        }
465
                }
466
        }
467
        while(rootFound);
468

UNCOV
469
        std::sort(points.values.begin(), points.values.end(), [](auto& p1, auto& p2){ return p1.Q < p2.Q; });
×
470
        // Remove spurious solutions that appeared due to multiplication by
471
        // a possibly vanishing denominator of the expression for zeta.
472
        points.values.erase(std::remove_if(points.values.begin(), points.values.end(),
×
UNCOV
473
                [&lhsAndDerivative,cosd,ddot,mudot,lhsScale](const auto& p)
×
474
                {
475
                        const auto [lhs, lhsPrime] = lhsAndDerivative(p.Q);
×
476
                        const auto denom = ddot*cos(p.Q) - mudot*cosd*sin(p.Q);
×
477
                        const bool toBeRemoved = abs(lhs / sqr(denom)) > 1e-10*lhsScale;
×
478
                        return toBeRemoved;
×
UNCOV
479
                }), points.values.end());
×
480

481
        return points;
×
UNCOV
482
}
×
483

UNCOV
484
SolarEclipseComputer::GeoPoint computeTimePoint(const EclipseBesselParameters& bp, const double earthOblateness,
×
485
                                                const double Q, const double zeta, const bool penumbra)
486
{
487
        // Reference: Explanatory Supplement to the Astronomical Ephemeris
488
        // and the American Ephemeris and Nautical Almanac, 3rd Edition (2013)
489

490
        const double f = earthOblateness;
×
491
        const double e2 = f*(2.-f);
×
492
        const double ff = 1./(1.-f);
×
493
        const double x = bp.elems.x, y = bp.elems.y, d = bp.elems.d, tf1 = bp.elems.tf1,
×
UNCOV
494
                     tf2 = bp.elems.tf2, L1 = bp.elems.L1, L2 = bp.elems.L2, mu = bp.elems.mu;
×
495

496
        using namespace std;
497
        const double sind = sin(d);
×
498
        const double cosd = cos(d);
×
499
        const double rho1 = sqrt(1-e2*sqr(cosd));
×
500
        const double rho2 = sqrt(1-e2*sqr(sind));
×
501
        const double sd1 = sind/rho1;
×
502
        const double cd1 = sqrt(1-e2)*cosd/rho1;
×
503
        const double sdd = e2*sind*cosd/(rho1*rho2);   // sin(d1-d2)
×
504
        const double cdd = sqrt(1-sqr(sdd));           // cos(d1-d2)
×
505
        const double tf = penumbra ? tf1 : tf2;
×
UNCOV
506
        const double L = penumbra ? L1 : L2;
×
507

UNCOV
508
        SolarEclipseComputer::GeoPoint coordinates{99., 0.};
×
509

510
        const double sinQ = sin(Q);
×
UNCOV
511
        const double cosQ = cos(Q);
×
512

513
        const double Lz = L - zeta*tf; // radius of shadow at distance zeta from the fundamental plane
×
514
        const double xi  = x - Lz * sinQ;
×
UNCOV
515
        const double eta = y - Lz * cosQ;
×
516

517
        const double eta1 = eta / rho1;
×
UNCOV
518
        const double zeta1 = (zeta / rho2 + eta1 * sdd) / cdd;
×
519

UNCOV
520
        if(abs(eta) > 1.0001 || abs(xi) > 1.0001 || abs(zeta) > 1.0001)
×
521
        {
522
                qWarning().noquote() << QString("Unnormalized vector (xi,eta,zeta) found: (%1, %2, %3); Q = %4°")
×
523
                                            .arg(xi,0,'g',17).arg(eta,0,'g',17).arg(zeta,0,'g',17)
×
UNCOV
524
                                            .arg(StelUtils::fmodpos(Q*180/M_PI, 360), 0,'g',17);
×
525
        }
526

527
        const double b = -eta1*sd1+zeta1*cd1;
×
528
        const double theta = atan2(xi,b)*M_180_PI;
×
529
        double lngDeg = theta-mu;
×
530
        lngDeg = StelUtils::fmodpos(lngDeg, 360.);
×
531
        if (lngDeg > 180.) lngDeg -= 360.;
×
532
        const double sfn1 = eta1*cd1+zeta1*sd1;
×
533
        const double cfn1 = sqrt(1.-sfn1*sfn1);
×
534
        const double tanLat = ff*sfn1/cfn1;
×
535
        coordinates.latitude = atan(tanLat)*M_180_PI;
×
UNCOV
536
        coordinates.longitude = lngDeg;
×
537

UNCOV
538
        return coordinates;
×
539
}
540

541
}
542

UNCOV
543
EclipseBesselElements calcSolarEclipseBessel()
×
544
{
545
        // Besselian elements
546
        // Source: Explanatory Supplement to the Astronomical Ephemeris
547
        // and the American Ephemeris and Nautical Almanac (1961)
548

549
        EclipseBesselElements out;
550

551
        StelCore* core = StelApp::getInstance().getCore();
×
552
        static SolarSystem* ssystem = GETSTELMODULE(SolarSystem);
×
553
        const bool topocentricWereEnabled = core->getUseTopocentricCoordinates();
×
UNCOV
554
        if(topocentricWereEnabled)
×
555
        {
556
                core->setUseTopocentricCoordinates(false);
×
UNCOV
557
                core->update(0);
×
558
        }
559

560
        double raMoon, deMoon, raSun, deSun;
561
        StelUtils::rectToSphe(&raSun, &deSun, ssystem->getSun()->getEquinoxEquatorialPos(core));
×
UNCOV
562
        StelUtils::rectToSphe(&raMoon, &deMoon, ssystem->getMoon()->getEquinoxEquatorialPos(core));
×
563

564
        double sdistanceAu = ssystem->getSun()->getEquinoxEquatorialPos(core).norm();
×
UNCOV
565
        const double earthRadius = ssystem->getEarth()->getEquatorialRadius()*AU;
×
566
        // Moon's distance in Earth's radius
UNCOV
567
        double mdistanceER = ssystem->getMoon()->getEquinoxEquatorialPos(core).norm() * AU / earthRadius;
×
568
        // Greenwich Apparent Sidereal Time
UNCOV
569
        const double gast = get_apparent_sidereal_time(core->getJD(), core->getJDE());
×
570

571
        // Avoid bug for special cases happen around Vernal Equinox
572
        double raDiff = StelUtils::fmodpos(raMoon-raSun, 2.*M_PI);
×
UNCOV
573
        if (raDiff>M_PI) raDiff-=2.*M_PI;
×
574

UNCOV
575
        constexpr double SunEarth = 109.12278;
×
576
        // ratio of Sun-Earth radius : 109.12278 = 696000/6378.1366
577
        // Earth's equatorial radius = 6378.1366
578
        // Source: IERS Conventions (2003)
579
        // https://www.iers.org/IERS/EN/Publications/TechnicalNotes/tn32.html
580

581
        // NASA's solar eclipse predictions use solar radius of 696,000 km
582
        // calculated from arctan of IAU 1976 solar radius (959.63 arcsec at 1 au).
583

584
        const double rss = sdistanceAu * 23454.7925; // from 1 AU/Earth's radius : 149597870.8/6378.1366
×
585
        const double b = mdistanceER / rss;
×
586
        const double a = raSun - ((b * cos(deMoon) * raDiff) / ((1 - b) * cos(deSun)));
×
587
        out.d = deSun - (b * (deMoon - deSun) / (1 - b));
×
588
        out.x = cos(deMoon) * sin((raMoon - a));
×
589
        out.x *= mdistanceER;
×
590
        out.y = cos(out.d) * sin(deMoon);
×
591
        out.y -= cos(deMoon) * sin(out.d) * cos((raMoon - a));
×
592
        out.y *= mdistanceER;
×
593
        double z = sin(deMoon) * sin(out.d);
×
594
        z += cos(deMoon) * cos(out.d) * cos((raMoon - a));
×
595
        z *= mdistanceER;
×
596
        const double k = 0.2725076;
×
UNCOV
597
        const double s = 0.272281;
×
598
        // Ratio of Moon/Earth's radius 0.2725076 is recommended by IAU for both k & s
599
        // s = 0.272281 is used by Fred Espenak/NASA for total eclipse to eliminate extreme cases
600
        // when the Moon's apparent diameter is very close to the Sun but cannot completely cover it.
601
        // we will use two values (same with NASA), because durations seem to agree with NASA.
602
        // Source: Solar Eclipse Predictions and the Mean Lunar Radius
603
        // http://eclipsewise.com/solar/SEhelp/SEradius.html
604

605
        // Parameters of the shadow cone
606
        const double f1 = asin((SunEarth + k) / (rss * (1. - b)));
×
607
        out.tf1 = tan(f1);
×
608
        const double f2 = asin((SunEarth - s) / (rss * (1. - b)));
×
609
        out.tf2 = tan(f2);
×
610
        out.L1 = z * out.tf1 + (k / cos(f1));
×
611
        out.L2 = z * out.tf2 - (s / cos(f2));
×
612
        out.mu = gast - a * M_180_PI;
×
UNCOV
613
        out.mu = StelUtils::fmodpos(out.mu, 360.);
×
614

UNCOV
615
        if(topocentricWereEnabled)
×
616
        {
617
                core->setUseTopocentricCoordinates(true);
×
UNCOV
618
                core->update(0);
×
619
        }
620

UNCOV
621
        return out;
×
622
}
623

UNCOV
624
EclipseBesselParameters calcBesselParameters(bool penumbra)
×
625
{
626
        EclipseBesselParameters out;
627
        StelCore* core = StelApp::getInstance().getCore();
×
628
        double JD = core->getJD();
×
629
        core->setJD(JD - 5./1440.);
×
630
        core->update(0);
×
631
        const auto ep1 = calcSolarEclipseBessel();
×
632
        const double x1 = ep1.x, y1 = ep1.y, d1 = ep1.d, mu1 = ep1.mu, L11 = ep1.L1, L21 = ep1.L2;
×
633
        core->setJD(JD + 5./1440.);
×
634
        core->update(0);
×
635
        const auto ep2 = calcSolarEclipseBessel();
×
UNCOV
636
        const double x2 = ep2.x, y2 = ep2.y, d2 = ep2.d, mu2 = ep2.mu, L12 = ep2.L1, L22 = ep2.L2;
×
637

638
        out.xdot = (x2-x1)*6.;
×
639
        out.ydot = (y2-y1)*6.;
×
640
        out.ddot = (d2-d1)*6.*M_PI_180;
×
641
        out.mudot = (mu2-mu1);
×
642
        if (out.mudot<0.) out.mudot += 360.; // make sure it is positive in case mu2 < mu1
×
643
        out.mudot = out.mudot*6.* M_PI_180;
×
644
        core->setJD(JD);
×
645
        core->update(0);
×
646
        out.elems = calcSolarEclipseBessel();
×
UNCOV
647
        const auto& ep = out.elems;
×
648
        double tf,L;
UNCOV
649
        if (penumbra)
×
650
        {
651
                L = ep.L1;
×
652
                tf = ep.tf1;
×
UNCOV
653
                out.ldot = (L12-L11)*6.;
×
654
        }
655
        else
656
        {
657
                L = ep.L2;
×
658
                tf = ep.tf2;
×
UNCOV
659
                out.ldot = (L22-L21)*6.;
×
660
        }
661
        out.etadot = out.mudot * ep.x * std::sin(ep.d);
×
662
        out.bdot = -(out.ydot-out.etadot);
×
UNCOV
663
        out.cdot = out.xdot + out.mudot * ep.y * std::sin(ep.d) + out.mudot * L * tf * std::cos(ep.d);
×
664

UNCOV
665
        return out;
×
666
}
667

UNCOV
668
void calcSolarEclipseData(double JD, double &dRatio, double &latDeg, double &lngDeg, double &altitude,
×
669
                          double &pathWidth, double &duration, double &magnitude)
670
{
671
        StelCore* core = StelApp::getInstance().getCore();
×
672
        const double currentJD = core->getJDOfLastJDUpdate(); // save current JD
×
673
        const qint64 millis = core->getMilliSecondsOfLastJDUpdate(); // keep time in sync
×
UNCOV
674
        const bool saveTopocentric = core->getUseTopocentricCoordinates();
×
675

676
        core->setUseTopocentricCoordinates(false);
×
677
        core->setJD(JD);
×
UNCOV
678
        core->update(0);
×
679

680
        static SolarSystem* ssystem = GETSTELMODULE(SolarSystem);
×
681
        static const double earthRadius = ssystem->getEarth()->getEquatorialRadius()*AU;
×
682
        static const double f = 1.0 - ssystem->getEarth()->getOneMinusOblateness(); // flattening
×
683
        static const double e2 = f*(2.-f);
×
UNCOV
684
        static const double ff = 1./(1.-f);
×
685

686
        const auto ep = calcSolarEclipseBessel();
×
687
        const double rho1 = sqrt(1. - e2 * cos(ep.d) * cos(ep.d));
×
688
        const double eta1 = ep.y / rho1;
×
689
        const double sd1 = sin(ep.d) / rho1;
×
690
        const double cd1 = sqrt(1. - e2) * cos(ep.d) / rho1;
×
691
        const double rho2 = sqrt(1.- e2 * sin(ep.d) * sin(ep.d));
×
692
        const double sd1d2 = e2*sin(ep.d)*cos(ep.d) / (rho1*rho2);
×
693
        const double cd1d2 = sqrt(1. - sd1d2 * sd1d2);
×
UNCOV
694
        const double p = 1. - ep.x * ep.x - eta1 * eta1;
×
695

UNCOV
696
        if (p > 0.) // Central eclipse : Moon's shadow axis is touching Earth
×
697
        {
698
                const double zeta1 = sqrt(p);
×
699
                const double zeta = rho2 * (zeta1 * cd1d2 - eta1 * sd1d2);
×
700
                const double L2a = ep.L2 - zeta * ep.tf2;
×
701
                const double b = -ep.y * sin(ep.d) + zeta * cos(ep.d);
×
702
                const double theta = atan2(ep.x, b) * M_180_PI;
×
703
                lngDeg = theta - ep.mu;
×
704
                lngDeg = StelUtils::fmodpos(lngDeg, 360.);
×
705
                if (lngDeg > 180.) lngDeg -= 360.;
×
706
                const double sfn1 = eta1 * cd1 + zeta1 * sd1;
×
707
                const double cfn1 = sqrt(1. - sfn1 * sfn1);
×
708
                latDeg = atan(ff * sfn1 / cfn1) / M_PI_180;
×
709
                const double L1a = ep.L1 - zeta * ep.tf1;
×
710
                magnitude = L1a / (L1a + L2a);
×
UNCOV
711
                dRatio = 1.+(magnitude-1.)*2.;
×
712

713
                core->setJD(JD - 5./1440.);
×
UNCOV
714
                core->update(0);
×
715

UNCOV
716
                const auto ep1 = calcSolarEclipseBessel();
×
717

718
                core->setJD(JD + 5./1440.);
×
UNCOV
719
                core->update(0);
×
720

UNCOV
721
                const auto ep2 = calcSolarEclipseBessel();
×
722

723
                // Hourly rate
724
                const double xdot = (ep2.x - ep1.x) * 6.;
×
725
                const double ydot = (ep2.y - ep1.y) * 6.;
×
726
                const double ddot = (ep2.d - ep1.d) * 6.;
×
727
                double mudot = (ep2.mu - ep1.mu);
×
728
                if (mudot<0.) mudot += 360.; // make sure it is positive in case ep2.mu < ep1.mu
×
UNCOV
729
                mudot = mudot * 6.* M_PI_180;
×
730

731
                // Duration of central eclipse in minutes
732
                const double etadot = mudot * ep.x * sin(ep.d) - ddot * zeta;
×
733
                const double xidot = mudot * (-ep.y * sin(ep.d) + zeta * cos(ep.d));
×
734
                const double n = sqrt((xdot - xidot) * (xdot - xidot) + (ydot - etadot) * (ydot - etadot));
×
UNCOV
735
                duration = L2a*120./n; // positive = annular eclipse, negative = total eclipse
×
736

737
                // Approximate altitude
UNCOV
738
                altitude = asin(cfn1*cos(ep.d)*cos(theta * M_PI_180)+sfn1*sin(ep.d)) / M_PI_180;
×
739

740
                // Path width in kilometers
741
                // Explanatory Supplement to the Astronomical Almanac
742
                // Seidelmann, P. Kenneth, ed. (1992). University Science Books. ISBN 978-0-935702-68-2
743
                // https://archive.org/details/131123ExplanatorySupplementAstronomicalAlmanac
744
                // Path width for central solar eclipses which only part of umbra/antumbra touches Earth
745
                // are too wide and could give a false impression, annular eclipse of 2003 May 31, for example.
746
                // We have to check this in the next step by calculating northern/southern limit of umbra/antumbra.
747
                // Don't show the path width if there is no northern limit or southern limit.
748
                // We will eventually have to calculate both limits, if we want to draw eclipse path on world map.
749
                const double p1 = zeta * zeta;
×
750
                const double p2 = ep.x * (xdot - xidot) / n;
×
751
                const double p3 = eta1 * (ydot - etadot) / n;
×
752
                const double p4 = (p2 + p3) * (p2 + p3);
×
UNCOV
753
                pathWidth = abs(earthRadius*2.*L2a/sqrt(p1+p4));
×
754
        }
755
        else  // Partial eclipse or non-central eclipse
756
        {
757
                const double yy1 = ep.y / rho1;
×
758
                double xi = ep.x / sqrt(ep.x * ep.x + yy1 * yy1);
×
759
                const double eta1 = yy1 / sqrt(ep.x * ep.x + yy1 * yy1);
×
760
                const double sd1 = sin(ep.d) / rho1;
×
761
                const double cd1 = sqrt(1.- e2) * cos(ep.d) / rho1;
×
762
                const double rho2 = sqrt(1.- e2 * sin(ep.d) * sin(ep.d));
×
763
                const double sd1d2 = e2 * sin(ep.d) * cos(ep.d) / (rho1 * rho2);
×
764
                double zeta = rho2 * (-(eta1) * sd1d2);
×
765
                const double b = -eta1 * sd1;
×
766
                double theta = atan2(xi, b);
×
767
                const double sfn1 = eta1*cd1;
×
768
                const double cfn1 = sqrt(1.- sfn1 * sfn1);
×
769
                double lat = ff * sfn1 / cfn1;
×
770
                lat = atan(lat);
×
771
                const double L1 = ep.L1 - zeta * ep.tf1;
×
772
                const double L2 = ep.L2 - zeta * ep.tf2;
×
773
                const double c = 1. / sqrt(1.- e2 * sin(lat) * sin(lat));
×
774
                const double s = (1.- e2) * c;
×
775
                const double rs = s * sin(lat);
×
776
                const double rc = c * cos(lat);
×
777
                xi = rc * sin(theta);
×
778
                const double eta = rs * cos(ep.d) - rc * sin(ep.d) * cos(theta);
×
779
                const double u = ep.x - xi;
×
780
                const double v = ep.y - eta;
×
781
                magnitude = (L1 - sqrt(u * u + v * v)) / (L1 + L2);
×
782
                dRatio = 1.+ (magnitude - 1.)* 2.;
×
783
                theta = theta / M_PI_180;
×
784
                lngDeg = theta - ep.mu;
×
785
                lngDeg = StelUtils::fmodpos(lngDeg, 360.);
×
786
                if (lngDeg > 180.) lngDeg -= 360.;
×
787
                latDeg = lat / M_PI_180;
×
788
                duration = 0.;
×
UNCOV
789
                pathWidth = 0.;
×
790
        }
791
        core->setJD(currentJD);
×
792
        core->setMilliSecondsOfLastJDUpdate(millis); // restore millis.
×
793
        core->setUseTopocentricCoordinates(saveTopocentric);
×
794
        core->update(0);
×
UNCOV
795
}
×
796

797
SolarEclipseComputer::SolarEclipseComputer(StelCore* core, StelLocaleMgr* localeMgr)
×
798
        : core(core)
×
UNCOV
799
        , localeMgr(localeMgr)
×
800
{
UNCOV
801
}
×
802

UNCOV
803
bool SolarEclipseComputer::bothPenumbraLimitsPresent(const double JDMid) const
×
804
{
805
        core->setJD(JDMid);
×
806
        core->update(0);
×
UNCOV
807
        const auto ep = calcSolarEclipseBessel();
×
808
        // FIXME: can the rise-set line exist at greatest eclipse but not exist at some other phase?
809
        // It seems that ellipticity of the Earth could result in this, because greatest eclipse is
810
        // defined relative to Earth's center rather than to its rim.
811
        const auto coordinates = getRiseSetLineCoordinates(true, ep.x, ep.y, ep.d, ep.L1, ep.mu);
×
UNCOV
812
        return coordinates.latitude > 90;
×
813
}
814

UNCOV
815
auto SolarEclipseComputer::generateEclipseMap(const double JDMid) const -> EclipseMapData
×
816
{
817
        const bool savedTopocentric = core->getUseTopocentricCoordinates();
×
818
        const double currentJD = core->getJD(); // save current JD
×
819
        core->setUseTopocentricCoordinates(false);
×
UNCOV
820
        core->update(0);
×
821

UNCOV
822
        EclipseMapData data;
×
823

824
        bool partialEclipse = false;
×
UNCOV
825
        bool nonCentralEclipse = false;
×
826
        double dRatio,altitude,pathWidth,duration,magnitude;
827
        core->setJD(JDMid);
×
828
        core->update(0);
×
829
        const auto ep = calcSolarEclipseBessel();
×
830
        const double x = ep.x, y = ep.y;
×
UNCOV
831
        double gamma = std::sqrt(x*x+y*y);
×
832
        // Type of eclipse
UNCOV
833
        if (abs(gamma) > 0.9972 && abs(gamma) < (1.5433 + ep.L2))
×
834
        {
UNCOV
835
                if (abs(gamma) < 0.9972 + abs(ep.L2))
×
836
                {
837
                        partialEclipse = false;
×
UNCOV
838
                        nonCentralEclipse = true; // non-central total/annular eclipse
×
839
                }
840
                else
UNCOV
841
                        partialEclipse = true;
×
842
        }
843
        const double JDP1 = getJDofContact(JDMid,true,true,true,true);
×
UNCOV
844
        const double JDP4 = getJDofContact(JDMid,false,true,true,true);
×
845

UNCOV
846
        double JDP2 = 0., JDP3 = 0.;
×
847
        GeoPoint coordinates;
848
        // Check northern/southern limits of penumbra at greatest eclipse
UNCOV
849
        const bool bothPenumbralLimits = bothPenumbraLimitsPresent(JDMid);
×
850

UNCOV
851
        if (bothPenumbralLimits)
×
852
        {
853
                JDP2 = getJDofContact(JDMid,true,true,true,false);
×
UNCOV
854
                JDP3 = getJDofContact(JDMid,false,true,true,false);
×
855
        }
856

857
        // Generate GE
858
        data.greatestEclipse.JD = JDMid;
×
UNCOV
859
        calcSolarEclipseData(JDMid,dRatio,data.greatestEclipse.latitude,data.greatestEclipse.longitude,altitude,pathWidth,duration,magnitude);
×
860

861
        // Generate P1
862
        data.firstContactWithEarth.JD = JDP1;
×
UNCOV
863
        calcSolarEclipseData(JDP1,dRatio,data.firstContactWithEarth.latitude,data.firstContactWithEarth.longitude,altitude,pathWidth,duration,magnitude);
×
864

865
        // Generate P4
866
        data.lastContactWithEarth.JD = JDP4;
×
UNCOV
867
        calcSolarEclipseData(JDP4,dRatio,data.lastContactWithEarth.latitude,data.lastContactWithEarth.longitude,altitude,pathWidth,duration,magnitude);
×
868

869
        // Northern/southern limits of penumbra
UNCOV
870
        computeNSLimitsOfShadow(JDP1, JDP4, true, data.penumbraLimits);
×
871

872
        // Eclipse begins/ends at sunrise/sunset curve
UNCOV
873
        if (bothPenumbralLimits)
×
874
        {
875
                bool first = true;
×
876
                for (int j = 0; j < 2; j++)
×
877
                {
878
                        if (j != 0) first = false;
×
879
                        // P1 to P2 curve
880
                        core->setJD(JDP2);
×
881
                        core->update(0);
×
882
                        auto ep = calcSolarEclipseBessel();
×
883
                        coordinates = getContactCoordinates(ep.x, ep.y, ep.d, ep.mu);
×
884
                        double latP2 = coordinates.latitude;
×
885
                        double lngP2 = coordinates.longitude;
×
886
                        auto& limit = data.riseSetLimits[j].emplace<EclipseMapData::TwoLimits>();
×
887

888
                        limit.p12curve.emplace_back(data.firstContactWithEarth.longitude,
×
889
                                                    data.firstContactWithEarth.latitude);
×
890
                        double JD = JDP1;
×
891
                        int i = 0;
×
892
                        while (JD < JDP2)
×
893
                        {
894
                                JD = JDP1 + i/1440.0;
×
895
                                core->setJD(JD);
×
896
                                core->update(0);
×
897
                                ep = calcSolarEclipseBessel();
×
898
                                coordinates = getRiseSetLineCoordinates(first, ep.x, ep.y, ep.d, ep.L1, ep.mu);
×
899
                                if (coordinates.latitude <= 90.)
×
900
                                        limit.p12curve.emplace_back(coordinates.longitude, coordinates.latitude);
×
901
                                i++;
×
902
                        }
903
                        limit.p12curve.emplace_back(lngP2, latP2);
×
904

905
                        // P3 to P4 curve
906
                        core->setJD(JDP3);
×
907
                        core->update(0);
×
908
                        ep = calcSolarEclipseBessel();
×
909
                        coordinates = getContactCoordinates(ep.x, ep.y, ep.d, ep.mu);
×
910
                        double latP3 = coordinates.latitude;
×
911
                        double lngP3 = coordinates.longitude;
×
912
                        limit.p34curve.emplace_back(lngP3, latP3);
×
913
                        JD = JDP3;
×
914
                        i = 0;
×
915
                        while (JD < JDP4)
×
916
                        {
917
                                JD = JDP3 + i/1440.0;
×
918
                                core->setJD(JD);
×
919
                                core->update(0);
×
920
                                ep = calcSolarEclipseBessel();
×
921
                                coordinates = getRiseSetLineCoordinates(first, ep.x, ep.y, ep.d, ep.L1, ep.mu);
×
922
                                if (coordinates.latitude <= 90.)
×
923
                                        limit.p34curve.emplace_back(coordinates.longitude, coordinates.latitude);
×
924
                                i++;
×
925
                        }
926
                        limit.p34curve.emplace_back(data.lastContactWithEarth.longitude,
×
927
                                                    data.lastContactWithEarth.latitude);
×
928
                }
929
        }
930
        else
931
        {
932
                // Only northern or southern limit exists
933
                // Draw the curve between P1 to P4
934
                bool first = true;
×
935
                for (int j = 0; j < 2; j++)
×
936
                {
937
                        if (j != 0) first = false;
×
938
                        auto& limit = data.riseSetLimits[j].emplace<EclipseMapData::SingleLimit>();
×
939
                        limit.curve.emplace_back(data.firstContactWithEarth.longitude,
×
940
                                                 data.firstContactWithEarth.latitude);
×
941
                        double JD = JDP1;
×
942
                        int i = 0;
×
943
                        while (JD < JDP4)
×
944
                        {
945
                                JD = JDP1 + i/1440.0;
×
946
                                core->setJD(JD);
×
947
                                core->update(0);
×
948
                                const auto ep = calcSolarEclipseBessel();
×
949
                                coordinates = getRiseSetLineCoordinates(first, ep.x, ep.y, ep.d, ep.L1, ep.mu);
×
950
                                if (coordinates.latitude <= 90.)
×
951
                                        limit.curve.emplace_back(coordinates.longitude, coordinates.latitude);
×
952
                                i++;
×
953
                        }
954
                        limit.curve.emplace_back(data.lastContactWithEarth.longitude,
×
955
                                                 data.lastContactWithEarth.latitude);
×
956
                }
957
        }
958

959
        // Curve of maximum eclipse at sunrise/sunset
960
        // There are two parts of the curve
961
        bool first = true;
×
962
        for (int j = 0; j < 2; j++)
×
963
        {
964
                if (j!= 0) first = false;
×
965
                if(bothPenumbralLimits)
×
966
                {
967
                        // The curve corresponding to P1-P2 time interval
968
                        data.maxEclipseAtRiseSet.emplace_back();
×
969
                        auto* curve = &data.maxEclipseAtRiseSet.back();
×
970
                        auto JDmin = JDP1;
×
971
                        auto JDmax = JDP2;
×
972
                        int numPoints = 5;
×
973
                        bool goodPointFound = false;
×
974
                        do
975
                        {
976
                                curve->clear();
×
977
                                numPoints = 2*numPoints+1;
×
978
                                const auto step = (JDmax-JDmin)/numPoints;
×
979
                                // We use an extended interval of n to include min and max values of JD. The internal
980
                                // values of JD are chosen in such a way that after increasing numPoints at the next
981
                                // iteration we'd check the points between the ones we checked in the previous
982
                                // iteration, thus more efficiently searching for good points, avoiding rechecking
983
                                // the same JD values.
984
                                for(int n = -1; n < numPoints+1; ++n)
×
985
                                {
986
                                        const auto JD = std::clamp(JDmin + step*(n+0.5), JDmin, JDmax);
×
987
                                        const auto coordinates = getMaximumEclipseAtRiseSet(first,JD);
×
988
                                        curve->emplace_back(JD, coordinates.longitude, coordinates.latitude);
×
989
                                        if (abs(coordinates.latitude) <= 90.)
×
990
                                                goodPointFound = true;
×
991
                                        else if(goodPointFound)
×
992
                                                break; // we've obtained a bad point after a good one, can refine now
×
993
                                }
994
                        }
995
                        while(!goodPointFound && numPoints < 500);
×
996

997
                        // We can't refine the curve if we have no usable points, so clear it (don't
998
                        // remove because we need it to match first and second branches).
999
                        if(!goodPointFound) curve->clear();
×
1000

1001
                        // The curve corresponding to P3-P4 time interval
1002
                        data.maxEclipseAtRiseSet.emplace_back();
×
1003
                        curve = &data.maxEclipseAtRiseSet.back();
×
1004
                        JDmin = JDP3;
×
1005
                        JDmax = JDP4;
×
1006
                        numPoints = 5;
×
1007
                        goodPointFound = false;
×
1008
                        do
1009
                        {
1010
                                curve->clear();
×
1011
                                numPoints = 2*numPoints+1;
×
1012
                                const double step = (JDmax-JDmin)/numPoints;
×
1013
                                // We use an extended interval of n to include min and max values of JD. The internal
1014
                                // values of JD are chosen in such a way that after increasing numPoints at the next
1015
                                // iteration we'd check the points between the ones we checked in the previous
1016
                                // iteration, thus more efficiently searching for good points, avoiding rechecking
1017
                                // the same JD values.
1018
                                for(int n = -1; n < numPoints+1; ++n)
×
1019
                                {
1020
                                        const double JD = std::clamp(JDmin + step*(n+0.5), JDmin, JDmax);
×
1021
                                        const auto coordinates = getMaximumEclipseAtRiseSet(first,JD);
×
1022
                                        curve->emplace_back(JD, coordinates.longitude, coordinates.latitude);
×
1023
                                        if (abs(coordinates.latitude) <= 90.)
×
1024
                                                goodPointFound = true;
×
1025
                                        else if(goodPointFound)
×
1026
                                                break; // we've obtained a bad point after a good one, can refine now
×
1027
                                }
1028
                        }
1029
                        while(!goodPointFound && numPoints < 500);
×
1030

1031
                        // We can't refine the curve if we have no usable points, so clear it (don't
1032
                        // remove because we need it to match first and second branches).
1033
                        if(!goodPointFound) curve->clear();
×
1034
                }
1035
                else
1036
                {
1037
                        // The curve corresponding to P1-P4 time interval
1038
                        data.maxEclipseAtRiseSet.emplace_back();
×
1039
                        auto*const curve = &data.maxEclipseAtRiseSet.back();
×
1040
                        const double JDmin = JDP1;
×
1041
                        const double JDmax = JDP4;
×
1042
                        int numPoints = 5;
×
1043
                        bool goodPointFound = false;
×
1044
                        do
1045
                        {
1046
                                curve->clear();
×
1047
                                numPoints = 2*numPoints+1;
×
1048
                                const auto step = (JDmax-JDmin)/numPoints;
×
1049
                                // We use an extended interval of n to include min and max values of JD. The internal
1050
                                // values of JD are chosen in such a way that after increasing numPoints at the next
1051
                                // iteration we'd check the points between the ones we checked in the previous
1052
                                // iteration, thus more efficiently searching for good points, avoiding rechecking
1053
                                // the same JD values.
1054
                                for(int n = -1; n < numPoints+1; ++n)
×
1055
                                {
1056
                                        const double JD = std::clamp(JDmin + step*(n+0.5), JDmin, JDmax);
×
1057
                                        const auto coordinates = getMaximumEclipseAtRiseSet(first,JD);
×
1058
                                        curve->emplace_back(JD, coordinates.longitude, coordinates.latitude);
×
1059
                                        if (abs(coordinates.latitude) <= 90.)
×
1060
                                                goodPointFound = true;
×
1061
                                        else if(goodPointFound)
×
1062
                                                break; // we've obtained a bad point after a good one, can refine now
×
1063
                                }
1064
                        }
1065
                        while(!goodPointFound && numPoints < 500);
×
1066

1067
                        // We can't refine the curve if we have no usable points, so clear it (don't
1068
                        // remove because we need it to match first and second branches).
1069
                        if(!goodPointFound) curve->clear();
×
1070
                }
1071
        }
1072
        // Refine at the beginnings and the ends of the lines so as to find the precise endpoints
1073
        for(unsigned c = 0; c < data.maxEclipseAtRiseSet.size(); ++c)
×
1074
        {
1075
                auto& points = data.maxEclipseAtRiseSet[c];
×
1076
                const bool first = c < data.maxEclipseAtRiseSet.size() / 2;
×
1077

1078
                // 1. Beginning of the line
1079
                const auto firstValidIt = std::find_if(points.begin(), points.end(),
×
1080
                                                       [](const auto& p){ return p.latitude <= 90; });
×
1081
                if (firstValidIt == points.end()) continue;
×
1082
                const int firstValidPos = firstValidIt - points.begin();
×
1083
                if (firstValidPos > 0)
×
1084
                {
1085
                        double lastInvalidTime = points[firstValidPos - 1].JD;
×
1086
                        double firstValidTime = points[firstValidPos].JD;
×
1087
                        // Bisect between these times. The sufficient number of iterations was found empirically.
1088
                        for (int n = 0; n < 15; ++n)
×
1089
                        {
1090
                                const auto currTime = (lastInvalidTime + firstValidTime) / 2;
×
1091
                                const auto coords = getMaximumEclipseAtRiseSet(first, currTime);
×
1092
                                if (coords.latitude > 90)
×
1093
                                {
1094
                                        lastInvalidTime = currTime;
×
1095
                                }
1096
                                else
1097
                                {
1098
                                        firstValidTime = currTime;
×
1099
                                        points.emplace_front(currTime, coords.longitude, coords.latitude);
×
1100
                                }
1101
                        }
1102
                }
1103

1104
                // 2. End of the line
1105
                const auto lastValidIt = std::find_if(points.rbegin(), points.rend(),
×
1106
                                                      [](const auto& p){ return p.latitude <= 90; });
×
1107
                if (lastValidIt == points.rend()) continue;
×
1108
                const int lastValidPos = points.size() - 1 - (lastValidIt - points.rbegin());
×
1109
                if (lastValidPos + 1u < points.size())
×
1110
                {
1111
                        double firstInvalidTime = points[lastValidPos + 1].JD;
×
1112
                        double lastValidTime = points[lastValidPos].JD;
×
1113
                        // Bisect between these times. The sufficient number of iterations was found empirically.
1114
                        for (int n = 0; n < 15; ++n)
×
1115
                        {
1116
                                const double currTime = (firstInvalidTime + lastValidTime) / 2;
×
1117
                                const auto coords = getMaximumEclipseAtRiseSet(first, currTime);
×
1118
                                if (coords.latitude > 90)
×
1119
                                {
1120
                                        firstInvalidTime = currTime;
×
1121
                                }
1122
                                else
1123
                                {
1124
                                        lastValidTime = currTime;
×
1125
                                        points.emplace_back(currTime, coords.longitude, coords.latitude);
×
1126
                                }
1127
                        }
1128
                }
1129

1130
                // 3. Cleanup: remove invalid points, sort by time increase
1131
                points.erase(std::remove_if(points.begin(), points.end(), [](const auto& p) { return p.latitude > 90; }),
×
1132
                             points.end());
×
1133
                std::sort(points.begin(), points.end(), [](const auto& a, const auto& b) { return a.JD < b.JD; });
×
1134

1135
                // Refine too long internal segments
1136
                bool newPointsInserted;
1137
                do
×
1138
                {
1139
                        newPointsInserted = false;
×
1140
                        const unsigned origNumPoints = points.size();
×
1141
                        for(unsigned n = 1; n < origNumPoints; ++n)
×
1142
                        {
1143
                                const double prevLat = points[n-1].latitude;
×
1144
                                const double currLat = points[n].latitude;
×
1145
                                const double prevLon = points[n-1].longitude;
×
1146
                                const double currLon = points[n].longitude;
×
1147
                                auto lonDiff = currLon - prevLon;
×
1148
                                while(lonDiff >  180) lonDiff -= 360;
×
1149
                                while(lonDiff < -180) lonDiff += 360;
×
1150
                                constexpr double admissibleStepDeg = 5;
×
1151
                                constexpr double admissibleStepDegSqr = admissibleStepDeg*admissibleStepDeg;
×
1152
                                // We want to sample more densely near the pole, where the rise/set curve may have
1153
                                // more features, so using unscaled longitude as one of the coordinates is on purpose.
1154
                                if(Vec2d(prevLat - currLat, lonDiff).normSquared() < admissibleStepDegSqr)
×
1155
                                        continue;
×
1156

1157
                                const double JD = (points[n-1].JD + points[n].JD) / 2;
×
1158
                                const auto coordinates = getMaximumEclipseAtRiseSet(first,JD);
×
1159
                                points.emplace_back(JD, coordinates.longitude, coordinates.latitude);
×
1160
                                newPointsInserted = true;
×
1161
                        }
1162
                        std::sort(points.begin(), points.end(), [](const auto& a, const auto& b) { return a.JD < b.JD; });
×
1163
                } while(newPointsInserted);
1164

1165
                // Connect the first and second branches of the lines
1166
                if(!first)
×
1167
                {
1168
                        const auto firstBranchIndex = c - data.maxEclipseAtRiseSet.size() / 2;
×
1169
                        auto& firstBranch  = data.maxEclipseAtRiseSet[firstBranchIndex];
×
1170
                        auto& secondBranch = data.maxEclipseAtRiseSet[c];
×
1171
                        if(firstBranch.empty() || secondBranch.empty())
×
1172
                                continue;
×
1173
                        // Connect the points that are closer to each other by time
1174
                        if(std::abs(secondBranch.back().JD - firstBranch.back().JD) < std::abs(secondBranch.front().JD - firstBranch.front().JD))
×
1175
                                secondBranch.emplace_back(firstBranch.back());
×
1176
                        else
1177
                                secondBranch.emplace_front(firstBranch.front());
×
1178
                }
1179
        }
1180

1181
        if (!partialEclipse)
×
1182
        {
1183
                double latDeg,lngDeg;
1184
                double JDC1 = JDMid, JDC2 = JDMid;
×
1185
                const double JDU1 = getJDofContact(JDMid,true,false,true,true); // beginning of external (ant)umbral contact
×
1186
                const double JDU4 = getJDofContact(JDMid,false,false,true,true); // end of external (ant)umbral contact
×
1187
                calcSolarEclipseData(JDC1,dRatio,latDeg,lngDeg,altitude,pathWidth,duration,magnitude);
×
UNCOV
1188
                if (!nonCentralEclipse)
×
1189
                {
1190
                        // C1
1191
                        double JD = getJDofContact(JDMid,true,false,false,true);
×
1192
                        JD = int(JD)+(int((JD-int(JD))*86400.)-1)/86400.;
×
1193
                        calcSolarEclipseData(JD,dRatio,latDeg,lngDeg,altitude,pathWidth,duration,magnitude);
×
1194
                        int steps = 0;
×
UNCOV
1195
                        while (pathWidth<0.0001 && steps<20)
×
1196
                        {
1197
                                JD += .1/86400.;
×
1198
                                calcSolarEclipseData(JD,dRatio,latDeg,lngDeg,altitude,pathWidth,duration,magnitude);
×
UNCOV
1199
                                steps += 1;
×
1200
                        }
1201
                        JDC1 = JD;
×
1202
                        core->setJD(JDC1);
×
1203
                        core->update(0);
×
1204
                        auto ep = calcSolarEclipseBessel();
×
1205
                        coordinates = getContactCoordinates(ep.x, ep.y, ep.d, ep.mu);
×
1206
                        double latC1 = coordinates.latitude;
×
UNCOV
1207
                        double lngC1 = coordinates.longitude;
×
1208

1209
                        // Generate C1
1210
                        data.centralEclipseStart.JD = JDC1;
×
1211
                        data.centralEclipseStart.longitude = lngC1;
×
UNCOV
1212
                        data.centralEclipseStart.latitude = latC1;
×
1213

1214
                        // C2
1215
                        JD = getJDofContact(JDMid,false,false,false,true);
×
1216
                        JD = int(JD)+(int((JD-int(JD))*86400.)+1)/86400.;
×
1217
                        calcSolarEclipseData(JD,dRatio,latDeg,lngDeg,altitude,pathWidth,duration,magnitude);
×
1218
                        steps = 0;
×
UNCOV
1219
                        while (pathWidth<0.0001 && steps<20)
×
1220
                        {
1221
                                JD -= .1/86400.;
×
1222
                                calcSolarEclipseData(JD,dRatio,latDeg,lngDeg,altitude,pathWidth,duration,magnitude);
×
UNCOV
1223
                                steps += 1;
×
1224
                        }
1225
                        JDC2 = JD;
×
1226
                        core->setJD(JDC2);
×
1227
                        core->update(0);
×
1228
                        ep = calcSolarEclipseBessel();
×
1229
                        coordinates = getContactCoordinates(ep.x, ep.y, ep.d, ep.mu);
×
1230
                        double latC2 = coordinates.latitude;
×
UNCOV
1231
                        double lngC2 = coordinates.longitude;
×
1232

1233
                        // Generate C2
1234
                        data.centralEclipseEnd.JD = JDC2;
×
1235
                        data.centralEclipseEnd.longitude = lngC2;
×
UNCOV
1236
                        data.centralEclipseEnd.latitude = latC2;
×
1237

1238
                        // Center line
1239
                        JD = JDC1;
×
1240
                        int i = 0;
×
1241
                        double dRatioC1 = dRatio;
×
1242
                        calcSolarEclipseData(JDMid,dRatio,latDeg,lngDeg,altitude,pathWidth,duration,magnitude);
×
1243
                        double dRatioMid = dRatio;
×
1244
                        calcSolarEclipseData(JDC2,dRatio,latDeg,lngDeg,altitude,pathWidth,duration,magnitude);
×
1245
                        double dRatioC2 = dRatio;
×
1246
                        if (dRatioC1 >= 1. && dRatioMid >= 1. && dRatioC2 >= 1.)
×
1247
                                data.eclipseType = EclipseMapData::EclipseType::Total;
×
1248
                        else if (dRatioC1 < 1. && dRatioMid < 1. && dRatioC2 < 1.)
×
UNCOV
1249
                                data.eclipseType = EclipseMapData::EclipseType::Annular;
×
1250
                        else
1251
                                data.eclipseType = EclipseMapData::EclipseType::Hybrid;
×
1252
                        data.centerLine.emplace_back(lngC1, latC1);
×
UNCOV
1253
                        while (JD+(1./1440.) < JDC2)
×
1254
                        {
1255
                                JD = JDC1 + i/1440.; // generate every one minute
×
1256
                                calcSolarEclipseData(JD,dRatio,latDeg,lngDeg,altitude,pathWidth,duration,magnitude);
×
1257
                                data.centerLine.emplace_back(lngDeg, latDeg);
×
UNCOV
1258
                                i++;
×
1259
                        }
UNCOV
1260
                        data.centerLine.emplace_back(lngC2, latC2);
×
1261
                }
1262
                //else // N.B. already done above!
1263
                //{
1264
                //        JDC1 = JDMid;
1265
                //        double = JDMid;
1266
                //}
1267

1268
                // Umbra/antumbra outline
1269
                // we want to draw (ant)umbral shadow on world map at exact times like 09:00, 09:10, 09:20, ...
1270
                double beginJD = int(JDU1)+(10.*int(1440.*(JDU1-int(JDU1))/10.)+10.)/1440.;
×
1271
                double endJD = int(JDU4)+(10.*int(1440.*(JDU4-int(JDU4))/10.))/1440.;
×
1272
                double JD = beginJD;
×
1273
                int i = 0;
×
1274
                double lat0 = 0., lon0 = 0.;
×
UNCOV
1275
                while (JD < endJD)
×
1276
                {
1277
                        JD = beginJD + i/144.; // generate every 10 minutes
×
1278
                        core->setJD(JD);
×
1279
                        core->update(0);
×
1280
                        const auto ep = calcSolarEclipseBessel();
×
1281
                        calcSolarEclipseData(JD,dRatio,latDeg,lngDeg,altitude,pathWidth,duration,magnitude);
×
1282
                        bool firstPoint = false;
×
1283
                        auto& outline = data.umbraOutlines.emplace_back();
×
1284
                        outline.JD = JD;
×
1285
                        if (dRatio>=1.)
×
1286
                                outline.eclipseType = EclipseMapData::EclipseType::Total;
×
1287
                        else
1288
                                outline.eclipseType = EclipseMapData::EclipseType::Annular;
×
1289
                        int pointNumber = 0;
×
1290
                        while (pointNumber < 60)
×
1291
                        {
1292
                                double angle = pointNumber*M_PI*2./60.;
×
1293
                                coordinates = getShadowOutlineCoordinates(angle, ep.x, ep.y, ep.d, ep.L2, ep.tf2, ep.mu);
×
1294
                                if (coordinates.latitude <= 90.)
×
1295
                                {
1296
                                        outline.curve.emplace_back(coordinates.longitude, coordinates.latitude);
×
1297
                                        if (!firstPoint)
×
1298
                                        {
1299
                                                lat0 = coordinates.latitude;
×
1300
                                                lon0 = coordinates.longitude;
×
1301
                                                firstPoint = true;
×
1302
                                        }
1303
                                }
1304
                                pointNumber++;
×
1305
                        }
1306
                        outline.curve.emplace_back(lon0, lat0); // completing the circle
×
1307
                        i++;
×
1308
                }
1309

1310
                // Northern/southern limits of umbra
1311
                computeNSLimitsOfShadow(JDP1, JDP4, false, data.umbraLimits);
×
1312
        }
1313

1314
        core->setJD(currentJD);
×
1315
        core->setUseTopocentricCoordinates(savedTopocentric);
×
1316
        core->update(0);
×
1317

1318
        return data;
×
1319
}
×
1320

1321
void SolarEclipseComputer::computeNSLimitsOfShadow(const double JDP1, const double JDP4, const bool penumbra,
×
1322
                                                   std::vector<std::vector<EclipseMapData::GeoTimePoint>>& limits) const
1323
{
1324
        // Reference: Explanatory Supplement to the Astronomical Ephemeris
1325
        // and the American Ephemeris and Nautical Almanac, 3rd Edition (2013)
1326

1327
        static SolarSystem* ssystem = GETSTELMODULE(SolarSystem);
×
1328
        static const double f = 1.0 - ssystem->getEarth()->getOneMinusOblateness(); // flattening
×
1329
        static const double e2 = f*(2.-f);
×
1330

1331
        const int iMax = std::ceil((JDP4-JDP1)*1440);
×
1332
        std::vector<ShadowLimitPoints> solutionsPerTime;
×
1333
        solutionsPerTime.reserve(iMax);
×
1334

1335
        constexpr double minutesToDays = 1./(24*60);
×
1336
        constexpr double secondsToDays = 1./(24*3600);
×
1337

1338
        // First sample the sets of Q values over all the time of the eclipse.
1339
        for(int i = 0; i < iMax; ++i)
×
1340
        {
1341
                const double JD = JDP1 + i*minutesToDays;
×
1342
                solutionsPerTime.emplace_back(getShadowLimitQs(core, JD, e2, penumbra));
×
1343
        }
1344

1345
        // Check that each set of solutions has an even number of solutions.
1346
        // If it's odd, the set is broken so should be removed.
1347
        for(unsigned i = 0; i < solutionsPerTime.size(); )
×
1348
        {
1349
                if(solutionsPerTime[i].values.size() % 2)
×
1350
                {
1351
                        qWarning() << "Found an odd number of values of Q:" << solutionsPerTime[i].values.size();
×
1352
                        solutionsPerTime.erase(solutionsPerTime.begin()+i);
×
1353
                }
1354
                else
1355
                {
1356
                        ++i;
×
1357
                }
1358
        }
1359

1360
        // Search for time points where number of Q values changes and
1361
        // refine each case to get closer to the point of the jump.
1362
        for(unsigned i = 1; i < solutionsPerTime.size(); ++i)
×
1363
        {
1364
                const auto& solA = solutionsPerTime[i-1];
×
1365
                const auto& solB = solutionsPerTime[i];
×
1366
                if(std::abs(solA.JD - solB.JD) <= 0.001*secondsToDays)
×
1367
                {
1368
                        // Already fine enough.
1369
                        continue;
×
1370
                }
1371

1372
                if(solA.values.size() != solB.values.size())
×
1373
                {
1374
                        const auto midJD = (solA.JD + solB.JD)/2;
×
1375
                        solutionsPerTime.insert(solutionsPerTime.begin()+i,
×
1376
                                                getShadowLimitQs(core, midJD, e2, penumbra));
×
1377
                        if(solutionsPerTime[i].values.size() % 2)
×
1378
                        {
1379
                                qWarning() << "Found an odd number of values of Q while searching for "
×
1380
                                              "JD of change in number of solutions:"
×
1381
                                           << solutionsPerTime[i].values.size();
×
1382
                        }
1383
                        // Retry with the first of the new intervals at the next iteration
1384
                        --i;
×
1385
                }
1386
        }
1387

1388
        // Search for time points where sign of zeta switches and
1389
        // refine each case to get closer to the zero crossing.
1390
        for(unsigned i = 1; i < solutionsPerTime.size(); ++i)
×
1391
        {
1392
                const auto& solA = solutionsPerTime[i-1];
×
1393
                const auto& solB = solutionsPerTime[i];
×
1394
                if(solA.values.size() != solB.values.size())
×
1395
                {
1396
                        // Even if there's a sign change, we've already refined these
1397
                        // points, so it's not a problem that we skip this case here.
1398
                        continue;
×
1399
                }
1400

1401
                if(std::abs(solA.JD - solB.JD) <= 0.001*secondsToDays)
×
1402
                {
1403
                        // Already fine enough.
1404
                        continue;
×
1405
                }
1406

1407
                for(int n = 0; n < solA.values.size(); ++n)
×
1408
                {
1409
                        // Refresh the references since they may have been
1410
                        // invalidated in a previous iteration by insertion
1411
                        const auto& solA = solutionsPerTime[i-1];
×
1412
                        const auto& solB = solutionsPerTime[i];
×
1413
                        if(solA.values.size() != solB.values.size())
×
1414
                        {
1415
                                // This can happen due to insertion of new points.
1416
                                // Let's skip this case for now.
1417
                                // FIXME: maybe we shouldn't.
1418
                                ++i;
×
1419
                                if(i >= solutionsPerTime.size())
×
1420
                                        break;
×
1421
                                continue;
×
1422
                        }
1423

1424
                        if(solA.values[n].zeta * solB.values[n].zeta < 0)
×
1425
                        {
1426
                                const auto midJD = (solA.JD + solB.JD)/2;
×
1427
                                solutionsPerTime.insert(solutionsPerTime.begin()+i,
×
1428
                                                        getShadowLimitQs(core, midJD, e2, penumbra));
×
1429
                                if(solutionsPerTime[i].values.size() % 2)
×
1430
                                {
1431
                                        qWarning() << "Found an odd number of values of Q while searching for "
×
1432
                                                      "JD of zeta sign change:"
×
1433
                                                   << solutionsPerTime[i].values.size();
×
1434
                                }
1435
                                // Retry with the first of the new intervals at the next iteration
1436
                                --i;
×
1437
                        }
1438
                }
1439
        }
1440

1441
        if(solutionsPerTime.empty()) return;
×
1442

1443
        // Now treat all runs of the same solution counts as simultaneous runs of multiple lines, where for each
1444
        // JD the point belonging to line n is the solution number n (the solutions are already sorted by Q).
1445

1446
        // 1. Compute the lines ignoring the sign of zeta
1447
        struct Point
1448
        {
1449
                GeoPoint geo;
1450
                double JD;
1451
                double zeta;
1452
                Point(const GeoPoint& geo, double JD, double zeta) : geo(geo), JD(JD), zeta(zeta) {}
×
1453
        };
1454
        std::vector<std::vector<Point>> lines;
×
1455
        lines.resize(solutionsPerTime[0].values.size());
×
1456
        for(unsigned i = 0, startN = 0; i < solutionsPerTime.size(); ++i)
×
1457
        {
1458
                const auto& sol = solutionsPerTime[i];
×
1459
                if(i > 0 && sol.values.size() != solutionsPerTime[i-1].values.size())
×
1460
                {
1461
                        startN = lines.size();
×
1462
                        lines.resize(lines.size() + sol.values.size());
×
1463
                }
1464
                for(unsigned n = 0; n < sol.values.size(); ++n)
×
1465
                {
1466
                        const double JD = sol.JD;
×
1467
                        const double Q = sol.values[n].Q;
×
1468
                        const double zeta = sol.values[n].zeta;
×
1469
                        const auto tp = computeTimePoint(sol.bp, f, Q, zeta, penumbra);
×
1470
                        lines[startN + n].emplace_back(tp, JD, zeta);
×
1471
                }
1472
        }
1473

1474
        // 2. Remove the points under the horizon (i.e. where zeta < 0)
1475
        for(unsigned n = 0; n < lines.size(); ++n)
×
1476
        {
1477
                auto& line = lines[n];
×
1478
                if(line.empty()) continue;
×
1479

1480
                const auto negZetaIt = std::find_if(line.begin(), line.end(), [](auto& p){ return p.zeta < 0; });
×
1481
                if(negZetaIt == line.end()) continue; // whole line is visible
×
1482

1483
                const auto nonNegZetaIt = std::find_if(line.begin(), line.end(),
×
1484
                                                       [](auto& p){ return p.zeta >= 0; });
×
1485
                if(nonNegZetaIt == line.end())
×
1486
                {
1487
                        // Whole line is under the horizon
1488
                        line.clear();
×
1489
                        continue;
×
1490
                }
1491

1492
                if(nonNegZetaIt == line.begin())
×
1493
                {
1494
                        // Line starts with non-negative zetas, then gets under the horizon.  Move the second
1495
                        // part of the line to a new line, skipping all leading points with negative zeta.
1496
                        const auto nextNonNegZetaIt = std::find_if(negZetaIt, line.end(),
×
1497
                                                                   [](auto& p){ return p.zeta >= 0; });
×
1498
                        if(nextNonNegZetaIt != line.end())
×
1499
                                lines.emplace_back(nextNonNegZetaIt, line.end());
×
1500
                        line.erase(negZetaIt, line.end());
×
1501
                }
1502
                else
1503
                {
1504
                        // Line starts with negative zetas and then there appear positive-zeta points.
1505
                        // Remove the negative-zeta head.
1506
                        const auto nextNonNegZetaIt = std::find_if(negZetaIt, line.end(),
×
1507
                                                                   [](auto& p){ return p.zeta >= 0; });
×
1508
                        line.erase(negZetaIt, nextNonNegZetaIt);
×
1509
                        if(!line.empty())
×
1510
                        {
1511
                                // The remaining points may still contain negative zetas,
1512
                                // so restart processing from the same line.
1513
                                --n;
×
1514
                        }
1515
                }
1516
        }
1517

1518
        // Finally, fill in the limits
1519
        limits.clear();
×
1520
        for(const auto& line : lines)
×
1521
        {
1522
                if(line.empty()) continue;
×
1523
                auto& limit = limits.emplace_back();
×
1524
                for(const auto& p : line)
×
1525
                {
1526
                        limit.emplace_back(p.JD, p.geo.longitude, p.geo.latitude);
×
1527
                }
1528
        }
1529
}
×
1530

1531
auto SolarEclipseComputer::getContactCoordinates(double x, double y, double d, double mu) const -> GeoPoint
×
1532
{
1533
        // Source: Explanatory Supplement to the Astronomical Ephemeris 
1534
        // and the American Ephemeris and Nautical Almanac (1961)
1535
        GeoPoint coordinates;
1536
        static SolarSystem* ssystem = GETSTELMODULE(SolarSystem);
×
1537
        static const double f = 1.0 - ssystem->getEarth()->getOneMinusOblateness(); // flattening
×
1538
        static const double e2 = f*(2.-f);
×
1539
        static const double ff = 1./(1.-f);
×
1540
        const double rho1 = std::sqrt(1.-e2*std::cos(d)*std::cos(d));
×
1541
        const double yy1 = y/rho1;
×
1542
        const double m1 = std::sqrt(x*x+yy1*yy1);
×
1543
        const double eta1 = yy1/m1;
×
1544
        const double sd1 = std::sin(d)/rho1;
×
1545
        const double cd1 = std::sqrt(1.-e2)*std::cos(d)/rho1;
×
1546
        const double theta = std::atan2(x/m1,-eta1*sd1)*M_180_PI;
×
1547
        double lngDeg = StelUtils::fmodpos(theta - mu + 180., 360.) - 180.;
×
1548
        double latDeg = ff*std::tan(std::asin(eta1*cd1));
×
1549
        coordinates.latitude = std::atan(latDeg)*M_180_PI;
×
1550
        coordinates.longitude = lngDeg;
×
1551
        return coordinates;
×
1552
}
1553

1554
auto SolarEclipseComputer::getRiseSetLineCoordinates(bool first, double x,double y,double d,double L,double mu) const -> GeoPoint
×
1555
{
1556
        // Reference for terminology and variable naming:
1557
        // Explanatory Supplement to the Astronomical Ephemeris and the
1558
        // American Ephemeris and Nautical Almanac, 3rd Edition (2013).
1559
        //
1560
        // The computation of the intersection is my own derivation, I haven't found it in the book.
1561
        static SolarSystem* ssystem = GETSTELMODULE(SolarSystem);
×
1562
        static const double f = 1.0 - ssystem->getEarth()->getOneMinusOblateness(); // flattening
×
1563
        static const double ff = 1./(1.-f);
×
1564

1565
        GeoPoint coordinates(99., 0.);
×
1566
        using namespace std;
1567
        const double sind = sin(d);
×
1568
        const double cosd = cos(d);
×
1569
        static const double e2 = f*(2.-f);
×
1570
        const double rho1 = sqrt(1-e2*sqr(cosd));
×
1571
        const double rho2 = sqrt(1-e2*sqr(sind));
×
1572
        const double sd1 = sind/rho1;
×
1573
        const double cd1 = sqrt(1-e2)*cosd/rho1;
×
1574
        const double sdd = e2*sind*cosd/(rho1*rho2);   // sin(d1-d2)
×
1575
        const double cdd = sqrt(1-sqr(sdd));           // cos(d1-d2)
×
1576

1577
        // Semi-minor axis of the elliptic cross section of the
1578
        // Earth in the fundamental plane (in Earth radii).
1579
        const double k = 1/sqrt(sqr(sind)+sqr(cosd)/(1-e2));
×
1580
        /*
1581
           We solve simultaneous equations: one for the ellipse of the Earth's border as
1582
           crossed by the fundamental plane, and the other is the circle of the shadow edge:
1583

1584
                                ⎧ xi^2+eta^2/k^2=1
1585
                                ⎨
1586
                                ⎩ (xi-x)^2+(eta-y)^2=L^2
1587

1588
            If we parametrize the solution for the Earth's border equation as
1589

1590
                                      xi=cos(t),
1591
                                      eta=k*sin(t),
1592

1593
            we'll get a single equation for the shadow border in terms of t:
1594

1595
                            (cos(t)-x)^2+(k*sin(t)-y)^2=L^2.
1596

1597
            This equation can be solved using Newton's method, which we'll now do.
1598
        */
1599
        const auto lhsAndDerivative = [=](const double t) -> std::pair<double,double>
×
1600
        {
1601
                const double cost = cos(t);
×
1602
                const double sint = sin(t);
×
1603
                const double lhs = sqr(cost-x) + sqr(k*sint-y) - sqr(L);
×
1604
                const double lhsPrime = 2*x*sint + 2*cost*((k*k-1)*sint - k*y);
×
1605
                return std::make_pair(lhs,lhsPrime);
×
1606
        };
×
1607

1608
        std::vector<double> ts;
×
1609
        double t = 0;
×
1610
        bool rootFound = false;
×
1611
        do
×
1612
        {
1613
                if(ts.size() == 2) break; // there can't be more than 2 roots, so don't spend time uselessly
×
1614

1615
                rootFound = false;
×
1616
                bool finalIteration = false;
×
1617
                for(int n = 0; n < 50; ++n)
×
1618
                {
1619
                        const auto [lhs, lhsPrime] = lhsAndDerivative(t);
×
1620

1621
                        // Cancel the known roots to avoid finding them instead of the remaining ones
1622
                        double newLHSPrime = lhsPrime;
×
1623
                        double newLHS = lhs;
×
1624
                        for(const auto rootT : ts)
×
1625
                        {
1626
                                // We need a 2pi-periodic root-canceling function,
1627
                                // so take a sine of half the difference.
1628
                                const double sinDiff = sin((t - rootT)/2);
×
1629
                                const double cosDiff = cos((t - rootT)/2);
×
1630

1631
                                newLHS /= sinDiff;
×
1632
                                newLHSPrime = (newLHSPrime - 0.5*cosDiff*newLHS)/sinDiff;
×
1633
                        }
1634

1635
                        if(abs(newLHS) < 1e-10)
×
1636
                                finalIteration = true;
×
1637

1638
                        const double deltaT = newLHS / newLHSPrime;
×
1639
                        if(newLHSPrime==0 || abs(deltaT) > 1000)
×
1640
                        {
1641
                                // We are shooting too far away, convergence may be too slow.
1642
                                // Let's try perturbing t and retrying.
1643
                                t += 0.01;
×
1644
                                finalIteration = false;
×
1645
                                continue;
×
1646
                        }
1647
                        t -= deltaT;
×
1648
                        t = StelUtils::fmodpos(t, 2*M_PI);
×
1649

1650
                        if(finalIteration)
×
1651
                        {
1652
                                ts.emplace_back(t);
×
1653
                                // Set a new initial value, but try to avoid setting it to 0 if the current
1654
                                // root is close to it (all values here are arbitrary in other respects).
1655
                                t = abs(t) > 0.5 ? 0 : -M_PI/2;
×
1656

1657
                                rootFound = true;
×
1658
                                break;
×
1659
                        }
1660
                }
1661
        }
1662
        while(rootFound);
1663

1664
        if(ts.empty()) return coordinates;
×
1665

1666
        double xi, eta;
1667
        if(ts.size() == 1)
×
1668
        {
1669
                const auto t = ts[0];
×
1670
                xi = cos(t);
×
1671
                eta = k*sin(t);
×
1672
        }
1673
        else
1674
        {
1675
                // Whether a solution is "first" or "second" depends on which side from the
1676
                // (0,0)-(x,y) line it is. To find this out we'll use the z component of
1677
                // the vector product (x,y,0)×(xi,eta,0).
1678
                const double sin_t0 = sin(ts[0]);
×
1679
                const double cos_t0 = cos(ts[0]);
×
1680
                const double sin_t1 = sin(ts[1]);
×
1681
                const double cos_t1 = cos(ts[1]);
×
1682

1683
                const double xi0 = cos_t0;
×
1684
                const double xi1 = cos_t1;
×
1685
                const double eta0 = k*sin_t0;
×
1686
                const double eta1 = k*sin_t1;
×
1687

1688
                const double vecProdZ0 = x * eta0 - y * xi0;
×
1689

1690
                const bool use0 = first ? vecProdZ0 < 0 : vecProdZ0 > 0;
×
1691

1692
                xi  = use0 ? xi0  : xi1;
×
1693
                eta = use0 ? eta0 : eta1;
×
1694
        }
1695

1696
        const double eta1 = eta / rho1;
×
1697
        const double zeta1 = (0 + eta1 * sdd) / cdd;
×
1698

1699
        const double b = -eta1*sd1+zeta1*cd1;
×
1700
        const double theta = atan2(xi,b)*M_180_PI;
×
1701
        double lngDeg = theta-mu;
×
1702
        lngDeg = StelUtils::fmodpos(lngDeg, 360.);
×
1703
        if (lngDeg > 180.) lngDeg -= 360.;
×
1704
        const double sfn1 = eta1*cd1+zeta1*sd1;
×
1705
        const double cfn1 = sqrt(1.-sfn1*sfn1);
×
1706
        const double tanLat = ff*sfn1/cfn1;
×
1707
        coordinates.latitude = atan(tanLat)*M_180_PI;
×
1708
        coordinates.longitude = lngDeg;
×
1709
        return coordinates;
×
1710
}
×
1711

1712
auto SolarEclipseComputer::getShadowOutlineCoordinates(double angle,double x,double y,double d,double L,double tf,double mu) const -> GeoPoint
×
1713
{
1714
        // Source: Explanatory Supplement to the Astronomical Ephemeris 
1715
        // and the American Ephemeris and Nautical Almanac (1961)
1716
        static SolarSystem* ssystem = GETSTELMODULE(SolarSystem);
×
1717
        static const double f = 1.0 - ssystem->getEarth()->getOneMinusOblateness(); // flattening
×
1718
        static const double e2 = f*(2.-f);
×
1719
        static const double ff = 1./(1.-f);
×
1720
        const double sinAngle=std::sin(angle);
×
1721
        const double cosAngle=std::cos(angle);
×
1722
        const double cosD = std::cos(d);
×
1723
        const double rho1 = std::sqrt(1.-e2*cosD*cosD);
×
1724
        const double sd1 = std::sin(d)/rho1;
×
1725
        const double cd1 = std::sqrt(1.-e2)*cosD/rho1;
×
1726

1727
        GeoPoint coordinates(99., 0.);
×
1728

1729
        double xi, eta1, zeta1 = 0;
×
1730
        for(int n = 0; n < 3; ++n)
×
1731
        {
1732
                double L1 = L-zeta1*tf;
×
1733
                xi = x-L1*sinAngle;
×
1734
                eta1 = (y-L1*cosAngle)/rho1;
×
1735
                const double zeta1sqr = 1.-xi*xi-eta1*eta1;
×
1736
                if (zeta1sqr < 0) return coordinates;
×
1737
                zeta1 = std::sqrt(zeta1sqr);
×
1738
        }
1739

1740
        double b = -eta1*sd1+zeta1*cd1;
×
1741
        double theta = std::atan2(xi,b)*M_180_PI;
×
1742
        double lngDeg = theta-mu;
×
1743
        lngDeg = StelUtils::fmodpos(lngDeg, 360.);
×
1744
        if (lngDeg > 180.) lngDeg -= 360.;
×
1745

1746
        double sfn1 = eta1*cd1+zeta1*sd1;
×
1747
        double cfn1 = std::sqrt(1.-sfn1*sfn1);
×
1748
        double tanLat = ff*sfn1/cfn1;
×
1749
        coordinates.latitude = std::atan(tanLat)*M_180_PI;
×
1750
        coordinates.longitude = lngDeg;
×
1751

1752
        return coordinates;
×
1753
}
1754

1755
auto SolarEclipseComputer::getMaximumEclipseAtRiseSet(bool first, double JD) const -> GeoPoint
×
1756
{
1757
        // Reference: Explanatory Supplement to the Astronomical Ephemeris
1758
        // and the American Ephemeris and Nautical Almanac, 3rd Edition (2013)
1759
        static SolarSystem* ssystem = GETSTELMODULE(SolarSystem);
×
1760
        static const double f = 1.0 - ssystem->getEarth()->getOneMinusOblateness(); // flattening
×
1761
        static const double e2 = f*(2.-f);
×
1762
        static const double ff = 1./(1.-f);
×
1763
        core->setJD(JD);
×
1764
        core->update(0);
×
1765
        const auto bp = calcBesselParameters(true);
×
1766
        const double bdot = bp.bdot, cdot = bp.cdot;
×
1767
        const double x = bp.elems.x, y = bp.elems.y, d = bp.elems.d, L1 = bp.elems.L1, mu = bp.elems.mu;
×
1768

1769
        using namespace std;
1770

1771
        const double sind = sin(d);
×
1772
        const double cosd = cos(d);
×
1773
        const double rho1 = sqrt(1-e2*sqr(cosd));
×
1774
        const double rho2 = sqrt(1-e2*sqr(sind));
×
1775
        const double sd1 = sind/rho1;
×
1776
        const double cd1 = sqrt(1-e2)*cosd/rho1;
×
1777
        const double sdd = e2*sind*cosd/(rho1*rho2);   // sin(d1-d2)
×
1778
        const double cdd = sqrt(1-sqr(sdd));           // cos(d1-d2)
×
1779

1780
        double qa = atan2(bdot,cdot);
×
1781
        if (!first) // there are two parts of the curve
×
1782
                qa += M_PI;
×
1783
        const double sgqa = x*cos(qa)-y*sin(qa);
×
1784

1785
        GeoPoint coordinates(99., 0.);
×
1786

1787
        // Iteration as described in equations (11.89) and (11.94) in the reference book
1788
        double rho = 1, gamma;
×
1789
        for(int n = 0; n < 3; ++n)
×
1790
        {
1791
                if(abs(sgqa / rho) > 1) return coordinates;
×
1792
                const double gqa = asin(sgqa / rho);
×
1793
                gamma = gqa+qa;
×
1794
                const double cosGamma = cos(gamma);
×
1795
                const double rho1sinGamma = rho1 * sin(gamma);
×
1796
                // simplified sin(atan2(rho1 * sin(gamma), cos(gamma)))
1797
                const double sinGammaPrime = rho1sinGamma / sqrt(sqr(rho1sinGamma)+sqr(cosGamma));
×
1798
                rho = sinGammaPrime / sin(gamma);
×
1799
        }
1800

1801
        const double xi = rho * sin(gamma);
×
1802
        const double eta = rho * cos(gamma);
×
1803

1804
        if (sqr(x-xi)+sqr(y-eta) > sqr(L1)) return coordinates;
×
1805

1806
        const double eta1 = eta / rho1;
×
1807
        const double zeta1 = (0 + eta1 * sdd) / cdd;
×
1808

1809
        const double b = -eta1*sd1+zeta1*cd1;
×
1810
        const double theta = atan2(xi,b)*M_180_PI;
×
1811
        double lngDeg = theta-mu;
×
1812
        lngDeg = StelUtils::fmodpos(lngDeg, 360.);
×
1813
        if (lngDeg > 180.) lngDeg -= 360.;
×
1814
        const double sfn1 = eta1*cd1+zeta1*sd1;
×
1815
        const double cfn1 = sqrt(1.-sfn1*sfn1);
×
1816
        const double tanLat = ff*sfn1/cfn1;
×
1817
        coordinates.latitude = atan(tanLat)*M_180_PI;
×
1818
        coordinates.longitude = lngDeg;
×
1819
        return coordinates;
×
1820
}
1821

1822
double SolarEclipseComputer::getDeltaTimeOfContact(double JD, bool beginning, bool penumbra, bool external, bool outerContact) const
×
1823
{
1824
        static SolarSystem* ssystem = GETSTELMODULE(SolarSystem);
×
1825
        static const double f = 1.0 - ssystem->getEarth()->getOneMinusOblateness(); // flattening
×
1826
        static const double e2 = f*(2.-f);
×
1827
        const int sign = outerContact ? 1 : -1; // there are outer & inner contacts
×
1828
        core->setJD(JD);
×
1829
        core->update(0);
×
1830
        const auto bp = calcBesselParameters(true);
×
1831
        const double xdot = bp.xdot;
×
1832
        double ydot = bp.ydot;
×
1833
        const double x = bp.elems.x, y = bp.elems.y, d = bp.elems.d, L1 = bp.elems.L1, L2 = bp.elems.L2;
×
1834
        const double rho1 = std::sqrt(1.-e2*std::cos(d)*std::cos(d));
×
1835
        double s,dt;
1836
        if (!penumbra)
×
1837
                ydot = ydot/rho1;
×
1838
        const double n = std::sqrt(xdot*xdot+ydot*ydot);
×
1839
        const double y1 = y/rho1;
×
1840
        const double m = std::sqrt(x*x+y*y);
×
1841
        const double m1 = std::sqrt(x*x+y1*y1);
×
1842
        const double rho = m/m1;
×
1843
        const double L = penumbra ? L1 : L2;
×
1844

1845
        if (external)
×
1846
        {
1847
                s = (x*ydot-y*xdot)/(n*(L+sign*rho)); // shadow's limb
×
1848
        }
1849
        else
1850
        {
1851
                s = (x*ydot-xdot*y1)/n; // center of shadow
×
1852
        }
1853
        double cs = std::sqrt(1.-s*s);
×
1854
        if (beginning)
×
1855
                cs *= -1.;
×
1856
        if (external)
×
1857
        {
1858
                dt = (L+sign*rho)*cs/n;
×
1859
                if (outerContact)
×
1860
                        dt -= ((x*xdot+y*ydot)/(n*n));
×
1861
                else
1862
                        dt = -((x*xdot+y*ydot)/(n*n))-dt;
×
1863
        }
1864
        else
1865
                dt = cs/n-((x*xdot+y1*ydot)/(n*n));
×
1866
        return dt;
×
1867
}
1868

1869
double SolarEclipseComputer::getJDofContact(double JD, bool beginning, bool penumbra, bool external, bool outerContact) const
×
1870
{
1871
        double dt = 1.;
×
1872
        int iterations = 0;
×
1873
        while (std::abs(dt)>(0.1/86400.) && (iterations < 10))
×
1874
        {
1875
                dt = getDeltaTimeOfContact(JD,beginning,penumbra,external,outerContact);
×
1876
                JD += dt/24.;
×
1877
                iterations++;
×
1878
        }
1879
        return JD;
×
1880
}
1881

1882
double SolarEclipseComputer::getJDofMinimumDistance(double JD) const
×
1883
{
1884
        const double currentJD = core->getJD(); // save current JD
×
1885
        double dt = 1.;
×
1886
        int iterations = 0;
×
1887
        while (std::abs(dt)>(0.1/86400.) && (iterations < 20)) // 0.1 second of accuracy
×
1888
        {
1889
                core->setJD(JD);
×
1890
                core->update(0);
×
1891
                const auto bp = calcBesselParameters(true);
×
1892
                const double xdot = bp.xdot, ydot = bp.ydot;
×
1893
                const double x = bp.elems.x, y = bp.elems.y;
×
1894
                double n2 = xdot*xdot + ydot*ydot;
×
1895
                dt = -(x*xdot + y*ydot)/n2;
×
1896
                JD += dt/24.;
×
1897
                iterations++;
×
1898
        }
1899
        core->setJD(currentJD);
×
1900
        core->update(0);
×
1901
        return JD;
×
1902
}
1903

1904
bool SolarEclipseComputer::generatePNGMap(const EclipseMapData& data, const QString& filePath) const
×
1905
{
1906
        QImage img(":/graphicGui/miscWorldMap.jpg");
×
1907

1908
        QPainter painter(&img);
×
1909
        painter.setRenderHint(QPainter::Antialiasing);
×
1910
        painter.translate(img.width() / 2., img.height() / 2.);
×
1911
        painter.scale(1,-1); // latitude grows upwards
×
1912

1913
        const double scale = img.width() / 360.;
×
1914
        const double penWidth = std::lround(img.width() / 2048.) / scale;
×
1915
        painter.scale(scale, scale);
×
1916

1917
        const auto updatePen = [&painter,penWidth](const EclipseMapData::EclipseType type =
×
1918
                                                           EclipseMapData::EclipseType::Undefined)
1919
        {
1920
                switch(type)
×
1921
                {
1922
                case EclipseMapData::EclipseType::Total:
×
1923
                        painter.setPen(QPen(*totalEclipseColor, penWidth));
×
1924
                        break;
×
1925
                case EclipseMapData::EclipseType::Annular:
×
1926
                        painter.setPen(QPen(*annularEclipseColor, penWidth));
×
1927
                        break;
×
1928
                case EclipseMapData::EclipseType::Hybrid:
×
1929
                        painter.setPen(QPen(*hybridEclipseColor, penWidth));
×
1930
                        break;
×
1931
                default:
×
1932
                        painter.setPen(QPen(*eclipseLimitsColor, penWidth));
×
1933
                        break;
×
1934
                }
1935
        };
×
1936

1937
        updatePen();
×
1938
        std::vector<QPointF> points;
×
1939
        for(const auto& penumbraLimit : data.penumbraLimits)
×
1940
        {
1941
                points.clear();
×
1942
                points.reserve(penumbraLimit.size());
×
1943
                for(const auto& p : penumbraLimit)
×
1944
                        points.emplace_back(p.longitude, p.latitude);
×
1945
                drawGeoLinesForEquirectMap(painter, points);
×
1946
        }
1947

1948
        for(const auto& riseSetLimit : data.riseSetLimits)
×
1949
        {
1950
                struct RiseSetLimitVisitor
1951
                {
1952
                        QPainter& painter;
1953
                        RiseSetLimitVisitor(QPainter& painter)
×
1954
                                : painter(painter)
×
1955
                        {
1956
                        }
×
1957
                        void operator()(const EclipseMapData::TwoLimits& limits) const
×
1958
                        {
1959
                                // P1-P2 curve
1960
                                std::vector<QPointF> points;
×
1961
                                points.reserve(limits.p12curve.size());
×
1962
                                for(const auto& p : limits.p12curve)
×
1963
                                        points.emplace_back(p.longitude, p.latitude);
×
1964
                                drawGeoLinesForEquirectMap(painter, points);
×
1965

1966
                                // P3-P4 curve
1967
                                points.clear();
×
1968
                                points.reserve(limits.p34curve.size());
×
1969
                                for(const auto& p : limits.p34curve)
×
1970
                                        points.emplace_back(p.longitude, p.latitude);
×
1971
                                drawGeoLinesForEquirectMap(painter, points);
×
1972
                        }
×
1973
                        void operator()(const EclipseMapData::SingleLimit& limit) const
×
1974
                        {
1975
                                std::vector<QPointF> points;
×
1976
                                points.reserve(limit.curve.size());
×
1977
                                for(const auto& p : limit.curve)
×
1978
                                        points.emplace_back(p.longitude, p.latitude);
×
1979
                                drawGeoLinesForEquirectMap(painter, points);
×
1980
                        }
×
1981
                };
1982
                std::visit(RiseSetLimitVisitor{painter}, riseSetLimit);
×
1983
        }
1984

1985
        for(const auto& curve : data.maxEclipseAtRiseSet)
×
1986
        {
1987
                points.clear();
×
1988
                points.reserve(curve.size());
×
1989
                for(const auto& p : curve)
×
1990
                        points.emplace_back(p.longitude, p.latitude);
×
1991
                drawGeoLinesForEquirectMap(painter, points);
×
1992
        }
1993

1994
        if(!data.centerLine.empty())
×
1995
        {
1996
                updatePen(data.eclipseType);
×
1997
                points.clear();
×
1998
                points.reserve(data.centerLine.size());
×
1999
                for(const auto& p : data.centerLine)
×
2000
                        points.emplace_back(p.longitude, p.latitude);
×
2001
                drawGeoLinesForEquirectMap(painter, points);
×
2002
        }
2003

2004
        for(const auto& umbraLimit : data.umbraLimits)
×
2005
        {
2006
                updatePen(data.eclipseType);
×
2007
                points.clear();
×
2008
                points.reserve(umbraLimit.size());
×
2009
                for(const auto& p : umbraLimit)
×
2010
                        points.emplace_back(p.longitude, p.latitude);
×
2011
                drawGeoLinesForEquirectMap(painter, points);
×
2012
        }
2013

2014
        for(const auto& outline : data.umbraOutlines)
×
2015
        {
2016
                updatePen(outline.eclipseType);
×
2017
                points.clear();
×
2018
                points.reserve(outline.curve.size());
×
2019
                for(const auto& p : outline.curve)
×
2020
                        points.emplace_back(p.longitude, p.latitude);
×
2021
                drawGeoLinesForEquirectMap(painter, points);
×
2022
        }
2023

2024
        return img.save(filePath, "PNG");
×
2025
}
×
2026

2027
void SolarEclipseComputer::generateKML(const EclipseMapData& data, const QString& dateString, QTextStream& stream) const
×
2028
{
2029
        using EclipseType = EclipseMapData::EclipseType;
2030

2031
        stream << "<?xml version='1.0' encoding='UTF-8'?>\n<kml xmlns='http://www.opengis.net/kml/2.2'>\n<Document>" << '\n';
×
2032
        stream << "<name>"+q_("Solar Eclipse")+" "+dateString+"</name>\n<description>"+q_("Created by Stellarium")+"</description>\n";
×
2033
        stream << "<Style id='Hybrid'>\n<LineStyle>\n<color>" << toKMLColorString(*hybridEclipseColor) << "</color>\n<width>1</width>\n</LineStyle>\n";
×
2034
        stream << "<PolyStyle>\n<color>" << toKMLColorString(*hybridEclipseColor) << "</color>\n</PolyStyle>\n</Style>\n";
×
2035
        stream << "<Style id='Total'>\n<LineStyle>\n<color>" << toKMLColorString(*totalEclipseColor) << "</color>\n<width>1</width>\n</LineStyle>\n";
×
2036
        stream << "<PolyStyle>\n<color>" << toKMLColorString(*totalEclipseColor) << "</color>\n</PolyStyle>\n</Style>\n";
×
2037
        stream << "<Style id='Annular'>\n<LineStyle>\n<color>" << toKMLColorString(*annularEclipseColor) << "</color>\n<width>1</width>\n</LineStyle>\n";
×
2038
        stream << "<PolyStyle>\n<color>" << toKMLColorString(*annularEclipseColor) << "</color>\n</PolyStyle>\n</Style>\n";
×
2039
        stream << "<Style id='PLimits'>\n<LineStyle>\n<color>" << toKMLColorString(*eclipseLimitsColor) << "</color>\n<width>1</width>\n</LineStyle>\n";
×
2040
        stream << "<PolyStyle>\n<color>" << toKMLColorString(*eclipseLimitsColor) << "</color>\n</PolyStyle>\n</Style>\n";
×
2041

2042
        {
2043
                const auto timeStr = localeMgr->getPrintableTimeLocal(data.greatestEclipse.JD, core->getUTCOffset(data.greatestEclipse.JD));
×
2044
                stream << "<Placemark>\n<name>"+q_("Greatest eclipse")+" ("+timeStr+")</name>\n<Point>\n<coordinates>";
×
2045
                stream << data.greatestEclipse.longitude << "," << data.greatestEclipse.latitude << ",0.0\n";
×
2046
                stream << "</coordinates>\n</Point>\n</Placemark>\n";
×
2047
        }
×
2048

2049
        {
2050
                const auto timeStr = localeMgr->getPrintableTimeLocal(data.firstContactWithEarth.JD, core->getUTCOffset(data.firstContactWithEarth.JD));
×
2051
                stream << "<Placemark>\n<name>"+q_("First contact with Earth")+" ("+timeStr+")</name>\n<Point>\n<coordinates>";
×
2052
                stream << data.firstContactWithEarth.longitude << "," << data.firstContactWithEarth.latitude << ",0.0\n";
×
2053
                stream << "</coordinates>\n</Point>\n</Placemark>\n";
×
2054
        }
×
2055

2056
        {
2057
                const auto timeStr = localeMgr->getPrintableTimeLocal(data.lastContactWithEarth.JD, core->getUTCOffset(data.lastContactWithEarth.JD));
×
2058
                stream << "<Placemark>\n<name>"+q_("Last contact with Earth")+" ("+timeStr+")</name>\n<Point>\n<coordinates>";
×
2059
                stream << data.lastContactWithEarth.longitude << "," << data.lastContactWithEarth.latitude << ",0.0\n";
×
2060
                stream << "</coordinates>\n</Point>\n</Placemark>\n";
×
2061
        }
×
2062

2063
        const auto startLinePlaceMark = [&stream](const QString& name, const EclipseMapData::EclipseType type = EclipseType::Undefined)
×
2064
        {
2065
                switch(type)
×
2066
                {
2067
                case EclipseMapData::EclipseType::Total:
×
2068
                        stream << "<Placemark>\n<name>" << name << "</name>\n<styleUrl>#Total</styleUrl>\n<LineString>\n<extrude>1</extrude>\n";
×
2069
                        break;
×
2070
                case EclipseMapData::EclipseType::Annular:
×
2071
                        stream << "<Placemark>\n<name>" << name << "</name>\n<styleUrl>#Annular</styleUrl>\n<LineString>\n<extrude>1</extrude>\n";
×
2072
                        break;
×
2073
                case EclipseMapData::EclipseType::Hybrid:
×
2074
                        stream << "<Placemark>\n<name>" << name << "</name>\n<styleUrl>#Hybrid</styleUrl>\n<LineString>\n<extrude>1</extrude>\n";
×
2075
                        break;
×
2076
                default:
×
2077
                        stream << "<Placemark>\n<name>" << name << "</name>\n<styleUrl>#PLimits</styleUrl>\n<LineString>\n<extrude>1</extrude>\n";
×
2078
                        break;
×
2079
                }
2080
        };
×
2081

2082
        for(const auto& penumbraLimit : data.penumbraLimits)
×
2083
        {
2084
                startLinePlaceMark("PenumbraLimit");
×
2085
                stream << "<tessellate>1</tessellate>\n<altitudeMode>absoluto</altitudeMode>\n<coordinates>\n";
×
2086
                for(const auto& p : penumbraLimit)
×
2087
                        stream << p.longitude << "," << p.latitude << ",0.0\n";
×
2088
                stream << "</coordinates>\n</LineString>\n</Placemark>\n";
×
2089
        }
2090

2091
        for(const auto& riseSetLimit : data.riseSetLimits)
×
2092
        {
2093
                struct RiseSetLimitVisitor
2094
                {
2095
                        QTextStream& stream;
2096
                        std::function<void(const QString&, EclipseType)> startLinePlaceMark;
2097
                        RiseSetLimitVisitor(QTextStream& stream, const std::function<void(const QString&, EclipseType)>& startLinePlaceMark)
×
2098
                                : stream(stream), startLinePlaceMark(startLinePlaceMark)
×
2099
                        {
2100
                        }
×
2101
                        void operator()(const EclipseMapData::TwoLimits& limits) const
×
2102
                        {
2103
                                // P1-P2 curve
2104
                                startLinePlaceMark("RiseSetLimit", EclipseType::Undefined);
×
2105
                                stream << "<tessellate>1</tessellate>\n<altitudeMode>absoluto</altitudeMode>\n<coordinates>\n";
×
2106
                                for(const auto& p : limits.p12curve)
×
2107
                                        stream << p.longitude << "," << p.latitude << ",0.0\n";
×
2108
                                stream << "</coordinates>\n</LineString>\n</Placemark>\n";
×
2109

2110
                                // P3-P4 curve
2111
                                startLinePlaceMark("RiseSetLimit", EclipseType::Undefined);
×
2112
                                stream << "<tessellate>1</tessellate>\n<altitudeMode>absoluto</altitudeMode>\n<coordinates>\n";
×
2113
                                for(const auto& p : limits.p34curve)
×
2114
                                        stream << p.longitude << "," << p.latitude << ",0.0\n";
×
2115
                                stream << "</coordinates>\n</LineString>\n</Placemark>\n";
×
2116
                        }
×
2117
                        void operator()(const EclipseMapData::SingleLimit& limit) const
×
2118
                        {
2119
                                startLinePlaceMark("RiseSetLimit", EclipseType::Undefined);
×
2120
                                stream << "<tessellate>1</tessellate>\n<altitudeMode>absoluto</altitudeMode>\n<coordinates>\n";
×
2121
                                for(const auto& p : limit.curve)
×
2122
                                        stream << p.longitude << "," << p.latitude << ",0.0\n";
×
2123
                                stream << "</coordinates>\n</LineString>\n</Placemark>\n";
×
2124
                        }
×
2125
                };
2126
                std::visit(RiseSetLimitVisitor{stream, startLinePlaceMark}, riseSetLimit);
×
2127
        }
2128

2129
        for(const auto& curve : data.maxEclipseAtRiseSet)
×
2130
        {
2131
                startLinePlaceMark("MaxEclipseSunriseSunset");
×
2132
                stream << "<tessellate>1</tessellate>\n<altitudeMode>absoluto</altitudeMode>\n<coordinates>\n";
×
2133
                for(const auto& p : curve)
×
2134
                        stream << p.longitude << "," << p.latitude << ",0.0\n";
×
2135
                stream << "</coordinates>\n</LineString>\n</Placemark>\n";
×
2136
        }
2137

2138
        if(data.centralEclipseStart.JD > 0)
×
2139
        {
2140
                const auto timeStr = localeMgr->getPrintableTimeLocal(data.centralEclipseStart.JD, core->getUTCOffset(data.centralEclipseStart.JD));
×
2141
                stream << "<Placemark>\n<name>"+q_("Central eclipse begins")+" ("+timeStr+")</name>\n<Point>\n<coordinates>";
×
2142
                stream << data.centralEclipseStart.longitude << "," << data.centralEclipseStart.latitude << ",0.0\n";
×
2143
                stream << "</coordinates>\n</Point>\n</Placemark>\n";
×
2144
        }
×
2145

2146
        if(data.centralEclipseEnd.JD > 0)
×
2147
        {
2148
                const auto timeStr = localeMgr->getPrintableTimeLocal(data.centralEclipseEnd.JD, core->getUTCOffset(data.centralEclipseEnd.JD));
×
2149
                stream << "<Placemark>\n<name>"+q_("Central eclipse ends")+" ("+timeStr+")</name>\n<Point>\n<coordinates>";
×
2150
                stream << data.centralEclipseEnd.longitude << "," << data.centralEclipseEnd.latitude << ",0.0\n";
×
2151
                stream << "</coordinates>\n</Point>\n</Placemark>\n";
×
2152
        }
×
2153

2154
        if(!data.centerLine.empty())
×
2155
        {
2156
                startLinePlaceMark("Center line", data.eclipseType);
×
2157
                stream << "<tessellate>1</tessellate>\n<altitudeMode>absoluto</altitudeMode>\n<coordinates>\n";
×
2158
                for(const auto& p : data.centerLine)
×
2159
                        stream << p.longitude << "," << p.latitude << ",0.0\n";
×
2160
                stream << "</coordinates>\n</LineString>\n</Placemark>\n";
×
2161
        }
2162

2163
        for(const auto& outline : data.umbraOutlines)
×
2164
        {
2165
                const auto timeStr = localeMgr->getPrintableTimeLocal(outline.JD, core->getUTCOffset(outline.JD));
×
2166
                startLinePlaceMark(timeStr, outline.eclipseType);
×
2167
                stream << "<tessellate>1</tessellate>\n<altitudeMode>absoluto</altitudeMode>\n<coordinates>\n";
×
2168
                for(const auto& p : outline.curve)
×
2169
                        stream << p.longitude << "," << p.latitude << ",0.0\n";
×
2170
                stream << "</coordinates>\n</LineString>\n</Placemark>\n";
×
2171
        }
×
2172

2173
        for(const auto& umbraLimit : data.umbraLimits)
×
2174
        {
2175
                startLinePlaceMark("Limit", data.eclipseType);
×
2176
                stream << "<tessellate>1</tessellate>\n<altitudeMode>absoluto</altitudeMode>\n<coordinates>\n";
×
2177
                for(const auto& p : umbraLimit)
×
2178
                        stream << p.longitude << "," << p.latitude << ",0.0\n";
×
2179
                stream << "</coordinates>\n</LineString>\n</Placemark>\n";
×
2180
        }
2181

2182
        stream << "</Document>\n</kml>\n";
×
2183
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc