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 }