File indexing completed on 2024-04-21 14:46:32

0001 /*
0002     SPDX-FileCopyrightText: 2010 Henry de Valence <hdevalence@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "projector.h"
0008 
0009 #include "ksutils.h"
0010 #ifdef KSTARS_LITE
0011 #include "skymaplite.h"
0012 #endif
0013 #include "skycomponents/skylabeler.h"
0014 
0015 namespace
0016 {
0017 void toXYZ(const SkyPoint *p, double *x, double *y, double *z)
0018 {
0019     double sinRa, sinDec, cosRa, cosDec;
0020 
0021     p->ra().SinCos(sinRa, cosRa);
0022     p->dec().SinCos(sinDec, cosDec);
0023     *x = cosDec * cosRa;
0024     *y = cosDec * sinRa;
0025     *z = sinDec;
0026 }
0027 }
0028 
0029 SkyPoint Projector::pointAt(double az)
0030 {
0031     SkyPoint p;
0032     p.setAz(az);
0033     p.setAlt(0.0);
0034     p.HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
0035     return p;
0036 }
0037 
0038 Projector::Projector(const ViewParams &p)
0039 {
0040     m_data = KStarsData::Instance();
0041     setViewParams(p);
0042     // Force clip polygon update
0043     updateClipPoly();
0044 }
0045 
0046 void Projector::setViewParams(const ViewParams &p)
0047 {
0048     m_vp = p;
0049 
0050     /** Precompute cached values */
0051     //Find Sin/Cos for focus point
0052     m_sinY0 = 0;
0053     m_cosY0 = 0;
0054     if (m_vp.useAltAz)
0055     {
0056         // N.B. We explicitly check useRefraction and not use
0057         // SkyPoint::altRefracted() here because it could be different
0058         // from Options::useRefraction() in some future use-case
0059         // --asimha
0060         SkyPoint::refract(m_vp.focus->alt(), m_vp.useRefraction).SinCos(m_sinY0, m_cosY0);
0061     }
0062     else
0063     {
0064         m_vp.focus->dec().SinCos(m_sinY0, m_cosY0);
0065     }
0066 
0067     double currentFOV = m_fov;
0068     //Find FOV in radians
0069     m_fov = sqrt(m_vp.width * m_vp.width + m_vp.height * m_vp.height) / (2 * m_vp.zoomFactor * dms::DegToRad);
0070     //Set checkVisibility variables
0071     double Ymax;
0072     m_xrange = 1.2 * m_fov / m_cosY0;
0073     if (m_vp.useAltAz)
0074     {
0075         Ymax     = fabs(SkyPoint::refract(m_vp.focus->alt().Degrees(), m_vp.useRefraction)) + m_fov;
0076     }
0077     else
0078     {
0079         Ymax     = fabs(m_vp.focus->dec().Degrees()) + m_fov;
0080     }
0081     m_isPoleVisible = (Ymax >= 90.0);
0082 
0083     // Only update clipping polygon if there is an FOV change
0084     if (currentFOV != m_fov)
0085         updateClipPoly();
0086 }
0087 
0088 double Projector::fov() const
0089 {
0090     return m_fov;
0091 }
0092 
0093 QPointF Projector::toScreen(const SkyPoint *o, bool oRefract, bool *onVisibleHemisphere) const
0094 {
0095     return KSUtils::vecToPoint(toScreenVec(o, oRefract, onVisibleHemisphere));
0096 }
0097 
0098 bool Projector::onScreen(const QPointF &p) const
0099 {
0100     return (0 <= p.x() && p.x() <= m_vp.width && 0 <= p.y() && p.y() <= m_vp.height);
0101 }
0102 
0103 bool Projector::onScreen(const Eigen::Vector2f &p) const
0104 {
0105     return onScreen(QPointF(p.x(), p.y()));
0106 }
0107 
0108 QPointF Projector::clipLine(SkyPoint *p1, SkyPoint *p2) const
0109 {
0110     return KSUtils::vecToPoint(clipLineVec(p1, p2));
0111 }
0112 
0113 Eigen::Vector2f Projector::clipLineVec(SkyPoint *p1, SkyPoint *p2) const
0114 {
0115     /* ASSUMES p1 was not clipped but p2 was.
0116      * Return the QPoint that barely clips in the line twixt p1 and p2.
0117      */
0118     //TODO: iteration = ceil( 0.5*log2( w^2 + h^2) )??
0119     //      also possibly rewrite this
0120     //     --hdevalence
0121     int iteration = 15; // For "perfect" clipping:
0122     // 2^iterations should be >= max pixels/line
0123     bool isVisible = true; // so we start at midpoint
0124     SkyPoint mid;
0125     Eigen::Vector2f oMid;
0126     double x, y, z, dx, dy, dz, ra, dec;
0127     int newx, newy, oldx, oldy;
0128     oldx = oldy = -10000; // any old value that is not the first omid
0129 
0130     toXYZ(p1, &x, &y, &z);
0131     // -jbb printf("\np1: %6.4f %6.4f %6.4f\n", x, y, z);
0132 
0133     toXYZ(p2, &dx, &dy, &dz);
0134 
0135     // -jbb printf("p2: %6.4f %6.4f %6.4f\n", dx, dy, dz);
0136     dx -= x;
0137     dy -= y;
0138     dz -= z;
0139     // Successive approximation to point on line that just clips.
0140     while (iteration-- > 0)
0141     {
0142         dx *= .5;
0143         dy *= .5;
0144         dz *= .5;
0145         if (!isVisible) // move back toward visible p1
0146         {
0147             x -= dx;
0148             y -= dy;
0149             z -= dz;
0150         }
0151         else // move out toward clipped p2
0152         {
0153             x += dx;
0154             y += dy;
0155             z += dz;
0156         }
0157 
0158         // -jbb printf("  : %6.4f %6.4f %6.4f\n", x, y, z);
0159         // [x, y, z] => [ra, dec]
0160         ra  = atan2(y, x);
0161         dec = asin(z / sqrt(x * x + y * y + z * z));
0162 
0163         mid = SkyPoint(ra * 12. / dms::PI, dec * 180. / dms::PI);
0164         mid.EquatorialToHorizontal(m_data->lst(), m_data->geo()->lat());
0165 
0166         oMid = toScreenVec(&mid, false, &isVisible);
0167         //AND the result with checkVisibility to clip things going below horizon
0168         isVisible &= checkVisibility(&mid);
0169         newx = (int)oMid.x();
0170         newy = (int)oMid.y();
0171 
0172         // -jbb printf("new x/y: %4d %4d", newx, newy);
0173         if ((oldx == newx) && (oldy == newy))
0174         {
0175             break;
0176         }
0177         oldx = newx;
0178         oldy = newy;
0179     }
0180     return oMid;
0181 }
0182 
0183 bool Projector::checkVisibility(const SkyPoint *p) const
0184 {
0185     //TODO deal with alternate projections
0186     //not clear how this depends on projection
0187     //FIXME do these heuristics actually work?
0188 
0189     double dX, dY;
0190 
0191     //Skip objects below the horizon if the ground is drawn
0192     /* Is the cost of this conversion actually less than drawing it anyways?
0193      * EquatorialToHorizontal takes 3 SinCos calls -- so 6 trig calls if not using GNU exts.
0194      */
0195     /*
0196     if( m_vp.fillGround  ) {
0197         if( !m_vp.useAltAz )
0198             p->EquatorialToHorizontal( m_data->lst(), m_data->geo()->lat() );
0199         if( p->alt().Degrees() < -1.0 ) return false;
0200     }
0201     */ //Here we hope that the point has already been 'synchronized'
0202     if (m_vp.fillGround /*&& m_vp.useAltAz*/ && p->alt().Degrees() <= SkyPoint::altCrit)
0203         return false;
0204 
0205     if (m_vp.useAltAz)
0206     {
0207         /** To avoid calculating refraction, we just use the unrefracted
0208             altitude and add a 2-degree 'safety factor' */
0209         dY = fabs(p->alt().Degrees() - m_vp.focus->alt().Degrees()) - 2.;
0210     }
0211     else
0212     {
0213         dY = fabs(p->dec().Degrees() - m_vp.focus->dec().Degrees());
0214     }
0215     if (m_isPoleVisible)
0216         dY *= 0.75; //increase effective FOV when pole visible.
0217     if (dY > m_fov)
0218         return false;
0219     if (m_isPoleVisible)
0220         return true;
0221 
0222     if (m_vp.useAltAz)
0223     {
0224         dX = fabs(p->az().Degrees() - m_vp.focus->az().Degrees());
0225     }
0226     else
0227     {
0228         dX = fabs(p->ra().Degrees() - m_vp.focus->ra().Degrees());
0229     }
0230     if (dX > 180.0)
0231         dX = 360.0 - dX; // take shorter distance around sky
0232 
0233     return dX < m_xrange;
0234 }
0235 
0236 // FIXME: There should be a MUCH more efficient way to do this (see EyepieceField for example)
0237 double Projector::findNorthPA(const SkyPoint *o, float x, float y) const
0238 {
0239     //Find position angle of North using a test point displaced to the north
0240     //displace by 100/zoomFactor radians (so distance is always 100 pixels)
0241     //this is 5730/zoomFactor degrees
0242     KStarsData *data = KStarsData::Instance();
0243     double newDec    = o->dec().Degrees() + 5730.0 / m_vp.zoomFactor;
0244     if (newDec > 90.0)
0245         newDec = 90.0;
0246     SkyPoint test(o->ra().Hours(), newDec);
0247     if (m_vp.useAltAz)
0248         test.EquatorialToHorizontal(data->lst(), data->geo()->lat());
0249     Eigen::Vector2f t = toScreenVec(&test);
0250     float dx    = t.x() - x;
0251     float dy    = y - t.y(); //backwards because QWidget Y-axis increases to the bottom (FIXME: Check)
0252     // float dx = dx_ * m_vp.rotationAngle.cos() - dy_ * m_vp.rotationAngle.sin();
0253     // float dy = dx_ * m_vp.rotationAngle.sin() + dy_ * m_vp.rotationAngle.cos();
0254     float north;
0255     if (dy)
0256     {
0257         north = atan2f(dx, dy) * 180.0 / dms::PI;
0258     }
0259     else
0260     {
0261         north = (dx > 0.0 ? -90.0 : 90.0);
0262     }
0263 
0264     return north;
0265 }
0266 
0267 double Projector::findPA(const SkyObject *o, float x, float y) const
0268 {
0269     return (findNorthPA(o, x, y) + o->pa());
0270 }
0271 
0272 // FIXME: There should be a MUCH more efficient way to do this (see EyepieceField for example)
0273 double Projector::findZenithPA(const SkyPoint *o, float x, float y) const
0274 {
0275     //Find position angle of North using a test point displaced to the north
0276     //displace by 100/zoomFactor radians (so distance is always 100 pixels)
0277     //this is 5730/zoomFactor degrees
0278     KStarsData *data = KStarsData::Instance();
0279     double newAlt    = o->alt().Degrees() + 5730.0 / m_vp.zoomFactor;
0280     if (newAlt > 90.0)
0281         newAlt = 90.0;
0282     SkyPoint test;
0283     test.setAlt(newAlt);
0284     test.setAz(o->az().Degrees());
0285     if (!m_vp.useAltAz)
0286         test.HorizontalToEquatorial(data->lst(), data->geo()->lat());
0287     Eigen::Vector2f t = toScreenVec(&test);
0288     float dx    = t.x() - x;
0289     float dy    = y - t.y(); //backwards because QWidget Y-axis increases to the bottom (FIXME: Check)
0290     // float dx = dx_ * m_vp.rotationAngle.cos() - dy_ * m_vp.rotationAngle.sin();
0291     // float dy = dx_ * m_vp.rotationAngle.sin() + dy_ * m_vp.rotationAngle.cos();
0292     float zenith;
0293     if (dy)
0294     {
0295         zenith = atan2f(dx, dy) * 180.0 / dms::PI;
0296     }
0297     else
0298     {
0299         zenith = (dx > 0.0 ? -90.0 : 90.0);
0300     }
0301 
0302     return zenith;
0303 }
0304 
0305 QVector<Eigen::Vector2f> Projector::groundPoly(SkyPoint *labelpoint, bool *drawLabel) const
0306 {
0307     QVector<Eigen::Vector2f> ground;
0308 
0309     static const QString horizonLabel = i18n("Horizon");
0310     float marginLeft, marginRight, marginTop, marginBot;
0311     SkyLabeler::Instance()->getMargins(horizonLabel, &marginLeft, &marginRight, &marginTop, &marginBot);
0312 
0313     //daz is 1/2 the width of the sky in degrees
0314     double daz = 90.;
0315     if (m_vp.useAltAz && m_vp.rotationAngle.reduce().Degrees() == 0.0)
0316     {
0317         daz = 0.5 * m_vp.width / (dms::DegToRad * m_vp.zoomFactor); //center to edge, in degrees
0318         if (type() == Projector::Orthographic)
0319         {
0320             daz = daz * 1.4;
0321         }
0322         daz = qMin(qreal(90.0), daz);
0323     }
0324 
0325     double faz = m_vp.focus->az().Degrees();
0326     double az1 = faz - daz;
0327     double az2 = faz + daz;
0328 
0329     bool allGround = true;
0330     bool allSky    = true;
0331 
0332     bool inverted = ((m_vp.rotationAngle + 90.0_deg).reduce().Degrees() > 180.);
0333 
0334     double inc = 1.0;
0335     //Add points along horizon
0336     for (double az = az1; az <= az2 + inc; az += inc)
0337     {
0338         SkyPoint p   = pointAt(az);
0339         bool visible = false;
0340         Eigen::Vector2f o   = toScreenVec(&p, false, &visible);
0341         if (visible)
0342         {
0343             ground.append(o);
0344             //Set the label point if this point is onscreen
0345             if (labelpoint && o.x() < marginRight && o.y() > marginTop && o.y() < marginBot)
0346                 *labelpoint = p;
0347 
0348             if (o.y() > 0.)
0349                 (inverted ? allSky : allGround) = false;
0350             if (o.y() < m_vp.height)
0351                 (inverted ? allGround : allSky) = false;
0352         }
0353     }
0354 
0355     if (allSky)
0356     {
0357         if (drawLabel)
0358             *drawLabel = false;
0359         return QVector<Eigen::Vector2f>();
0360     }
0361 
0362     if (allGround)
0363     {
0364         ground.clear();
0365         ground.append(Eigen::Vector2f(-10., -10.));
0366         ground.append(Eigen::Vector2f(m_vp.width + 10., -10.));
0367         ground.append(Eigen::Vector2f(m_vp.width + 10., m_vp.height + 10.));
0368         ground.append(Eigen::Vector2f(-10., m_vp.height + 10.));
0369         if (drawLabel)
0370             *drawLabel = false;
0371         return ground;
0372     }
0373 
0374     //In Gnomonic projection, or if sufficiently zoomed in, we can complete
0375     //the ground polygon by simply adding offscreen points
0376     //FIXME: not just gnomonic
0377     if (daz < 25.0 || type() == Projector::Gnomonic)
0378     {
0379         const float completion_height = (inverted ?
0380                                          -10.f : m_vp.height + 10.f);
0381         ground.append(Eigen::Vector2f(m_vp.width + 10.f, ground.last().y()));
0382         ground.append(Eigen::Vector2f(m_vp.width + 10.f, completion_height));
0383         ground.append(Eigen::Vector2f(-10.f, completion_height));
0384         ground.append(Eigen::Vector2f(-10.f, ground.first().y()));
0385     }
0386     else
0387     {
0388         double r = m_vp.zoomFactor * radius();
0389         double t1 =
0390             atan2(-1. * (ground.last().y() - 0.5 * m_vp.height), ground.last().x() - 0.5 * m_vp.width) / dms::DegToRad;
0391         double t2 = t1 - 180.;
0392         for (double t = t1; t >= t2; t -= inc) //step along circumference
0393         {
0394             dms a(t);
0395             double sa(0.), ca(0.);
0396             a.SinCos(sa, ca);
0397             ground.append(Eigen::Vector2f(0.5 * m_vp.width + r * ca, 0.5 * m_vp.height - r * sa));
0398         }
0399     }
0400 
0401     if (drawLabel)
0402         *drawLabel = true;
0403     return ground;
0404 }
0405 
0406 void Projector::updateClipPoly()
0407 {
0408     m_clipPolygon.clear();
0409 
0410     double r   = m_vp.zoomFactor * radius();
0411     double t1  = 0;
0412     double t2  = 360;
0413     double inc = 1.0;
0414     for (double t = t1; t <= t2; t += inc)
0415     {
0416         //step along circumference
0417         dms a(t);
0418         double sa(0.), ca(0.);
0419         a.SinCos(sa, ca);
0420         m_clipPolygon << QPointF(0.5 * m_vp.width + r * ca, 0.5 * m_vp.height - r * sa);
0421     }
0422 }
0423 
0424 QPolygonF Projector::clipPoly() const
0425 {
0426     return m_clipPolygon;
0427 }
0428 
0429 bool Projector::unusablePoint(const QPointF &p) const
0430 {
0431     //r0 is the angular size of the sky horizon, in radians
0432     double r0 = radius();
0433     //If the zoom is high enough, all points are usable
0434     //The center-to-corner distance, in radians
0435     double r = 0.5 * 1.41421356 * m_vp.width / m_vp.zoomFactor;
0436     if (r < r0)
0437         return false;
0438     //At low zoom, we have to determine whether the point is beyond the sky horizon
0439     //Convert pixel position to x and y offsets in radians
0440 
0441     // N.B. Technically, rotation does not affect the dx² + dy²
0442     // metric, but we use the derst method for uniformity; this
0443     // function is not perf critical
0444     auto p_ = derst(p.x(), p.y());
0445     double dx = p_[0], dy = p_[1];
0446     return (dx * dx + dy * dy) > r0 * r0;
0447 }
0448 
0449 SkyPoint Projector::fromScreen(const QPointF &p, dms *LST, const dms *lat, bool onlyAltAz) const
0450 {
0451     dms c;
0452     double sinc, cosc;
0453     /** N.B. We don't cache these sin/cos values in the inverse
0454       * projection because it causes 'shaking' when moving the sky.
0455       */
0456     double sinY0, cosY0;
0457     //Convert pixel position to x and y offsets in radians
0458     auto p_ = derst(p.x(), p.y());
0459     double dx = p_[0], dy = p_[1];
0460 
0461     double r = sqrt(dx * dx + dy * dy);
0462     c.setRadians(projectionL(r));
0463     c.SinCos(sinc, cosc);
0464 
0465     if (m_vp.useAltAz)
0466     {
0467         dx = -1.0 * dx; //Azimuth goes in opposite direction compared to RA
0468         SkyPoint::refract(m_vp.focus->alt(), m_vp.useRefraction).SinCos(sinY0, cosY0);
0469     }
0470     else
0471     {
0472         m_vp.focus->dec().SinCos(sinY0, cosY0);
0473     }
0474 
0475     double Y    = asin(cosc * sinY0 + (r == 0 ? 0 : (dy * sinc * cosY0) / r));
0476     double atop = dx * sinc;
0477     double abot = r * cosY0 * cosc - dy * sinY0 * sinc;
0478     double A    = atan2(atop, abot);
0479 
0480     SkyPoint result;
0481     if (m_vp.useAltAz)
0482     {
0483         dms alt, az;
0484         alt.setRadians(Y);
0485         az.setRadians(A + m_vp.focus->az().radians());
0486         if (m_vp.useRefraction)
0487             alt = SkyPoint::unrefract(alt);
0488         result.setAlt(alt);
0489         result.setAz(az);
0490         if (onlyAltAz)
0491             return result;
0492         result.HorizontalToEquatorial(LST, lat);
0493     }
0494     else
0495     {
0496         dms ra, dec;
0497         dec.setRadians(Y);
0498         ra.setRadians(A + m_vp.focus->ra().radians());
0499         result.set(ra.reduce(), dec);
0500         result.EquatorialToHorizontal(LST, lat);
0501     }
0502 
0503     return result;
0504 }
0505 
0506 Eigen::Vector2f Projector::toScreenVec(const SkyPoint *o, bool oRefract, bool *onVisibleHemisphere) const
0507 {
0508     double Y, dX;
0509     double sindX, cosdX, sinY, cosY;
0510 
0511     oRefract &= m_vp.useRefraction;
0512     if (m_vp.useAltAz)
0513     {
0514         if (oRefract)
0515             Y = SkyPoint::refract(o->alt()).radians(); //account for atmospheric refraction
0516         else
0517             Y = o->alt().radians();
0518         dX = m_vp.focus->az().radians() - o->az().radians();
0519     }
0520     else
0521     {
0522         dX = o->ra().radians() - m_vp.focus->ra().radians();
0523         Y  = o->dec().radians();
0524     }
0525 
0526     if (!(std::isfinite(Y) && std::isfinite(dX)))
0527     {
0528         return Eigen::Vector2f(0, 0);
0529 
0530         // JM: Enable this again later when trying to find a solution for it
0531         //     As it is now creating too much noise in the log file.
0532         /*
0533         qDebug() << Q_FUNC_INFO << "Assert in Projector::toScreenVec failed!";
0534         qDebug() << Q_FUNC_INFO << "using AltAz?" << m_vp.useAltAz << " Refract? " << oRefract;
0535         const SkyObject *obj;
0536         qDebug() << Q_FUNC_INFO << "Point supplied has RA0 = " << o->ra0().toHMSString() << " Dec0 = " << o->dec0().toDMSString() << "; alt = " << o->alt().toDMSString() << "; az = " << o->az().toDMSString();
0537         if ( (obj = dynamic_cast<const SkyObject *>(o) ) ) {
0538             qDebug() << Q_FUNC_INFO << "Point is object with name = " << obj->name() << " longname = " << obj->longname();
0539         }
0540         qDebug() << Q_FUNC_INFO << "dX = " << dX << " and isfinite(dX) is" << std::isfinite(dX);
0541         qDebug() << Q_FUNC_INFO << "Y = " << Y << " and isfinite(Y) is" << std::isfinite(Y);
0542 
0543         //Q_ASSERT( false );
0544         */
0545     }
0546 
0547     dX = KSUtils::reduceAngle(dX, -dms::PI, dms::PI);
0548 
0549     //Convert dX, Y coords to screen pixel coords, using GNU extension if available
0550 #ifdef HAVE_SINCOS
0551     sincos(dX, &sindX, &cosdX);
0552     sincos(Y, &sinY, &cosY);
0553 #else
0554     sindX = sin(dX);
0555     cosdX = cos(dX);
0556     sinY  = sin(Y);
0557     cosY  = cos(Y);
0558 #endif
0559 
0560     //c is the cosine of the angular distance from the center
0561     double c = m_sinY0 * sinY + m_cosY0 * cosY * cosdX;
0562 
0563     //If c is less than 0.0, then the "field angle" (angular distance from the focus)
0564     //is more than 90 degrees.  This is on the "back side" of the celestial sphere
0565     //and should not be drawn.
0566     if (onVisibleHemisphere)
0567         *onVisibleHemisphere =
0568             (c >
0569              cosMaxFieldAngle()); // TODO: Isn't it more efficient to bypass the final calculation below if the object is not visible?
0570 
0571     double k = projectionK(c);
0572 
0573     auto p = rst(k * cosY * sindX, k * (m_cosY0 * sinY - m_sinY0 * cosY * cosdX));
0574 
0575 #ifdef KSTARS_LITE
0576     double skyRotation = SkyMapLite::Instance()->getSkyRotation();
0577     // FIXME: Port above to change the m_vp.rotationAngle, or
0578     // deprecate it
0579     Q_ASSERT(false);
0580 #endif
0581     return p;
0582 }