File indexing completed on 2024-04-14 03:43:11

0001 /*
0002     SPDX-FileCopyrightText: 2022 Jasem Mutlaq <mutlaqja@ikarustech.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include <QPainter>
0008 
0009 #include "mosaictiles.h"
0010 #include "kstarsdata.h"
0011 #include "Options.h"
0012 
0013 MosaicTiles::MosaicTiles() : SkyObject()
0014 {
0015     setName(QLatin1String("Mosaic Tiles"));
0016 
0017     m_Brush.setStyle(Qt::NoBrush);
0018     m_Pen.setColor(QColor(200, 200, 200, 100));
0019     m_Pen.setWidth(1);
0020 
0021     m_TextBrush.setStyle(Qt::SolidPattern);
0022     m_TextPen.setColor(Qt::red);
0023     m_TextPen.setWidth(2);
0024 
0025     m_FocalLength = Options::telescopeFocalLength();
0026     m_FocalReducer = Options::telescopeFocalReducer();
0027     m_CameraSize.setWidth(Options::cameraWidth());
0028     m_CameraSize.setHeight(Options::cameraHeight());
0029     m_PixelSize.setWidth(Options::cameraPixelWidth());
0030     m_PixelSize.setHeight(Options::cameraPixelHeight());
0031     m_PositionAngle = Options::cameraRotation();
0032 
0033     // Initially for 1x1 Grid
0034     m_MosaicFOV = m_CameraFOV = calculateCameraFOV();
0035 
0036     createTiles(false);
0037 }
0038 
0039 MosaicTiles::~MosaicTiles()
0040 {
0041 }
0042 
0043 bool MosaicTiles::isValid() const
0044 {
0045     return m_MosaicFOV.width() > 0 && m_MosaicFOV.height() > 0;
0046 }
0047 
0048 void MosaicTiles::setPositionAngle(double value)
0049 {
0050     m_PositionAngle = value;
0051 }
0052 
0053 void MosaicTiles::setOverlap(double value)
0054 {
0055     m_Overlap = (value < 0) ? 0 : (value > 100) ? 100 : value;
0056 }
0057 
0058 std::shared_ptr<MosaicTiles::OneTile> MosaicTiles::oneTile(int row, int col)
0059 {
0060     int offset = row * m_GridSize.width() + col;
0061 
0062     if (offset < 0 || offset >= m_Tiles.size())
0063         return nullptr;
0064 
0065     return m_Tiles[offset];
0066 }
0067 
0068 bool MosaicTiles::fromXML(const QString &filename)
0069 {
0070     QFile sFile;
0071     sFile.setFileName(filename);
0072 
0073     if (!sFile.open(QIODevice::ReadOnly))
0074         return false;
0075 
0076     LilXML *xmlParser = newLilXML();
0077     char errmsg[2048] = {0};
0078     XMLEle *root = nullptr;
0079     XMLEle *ep   = nullptr;
0080     char c;
0081 
0082     m_OperationMode = MODE_OPERATION;
0083 
0084     m_Tiles.clear();
0085 
0086     // We expect all data read from the XML to be in the C locale - QLocale::c()
0087     QLocale cLocale = QLocale::c();
0088 
0089     bool mosaicInfoFound = false;
0090     int index = 1;
0091 
0092     m_TrackChecked = m_FocusChecked = m_AlignChecked = m_GuideChecked = false;
0093 
0094     while (sFile.getChar(&c))
0095     {
0096         root = readXMLEle(xmlParser, c, errmsg);
0097 
0098         if (root)
0099         {
0100             for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
0101             {
0102                 const char *tag = tagXMLEle(ep);
0103                 if (!strcmp(tag, "Mosaic"))
0104                 {
0105                     mosaicInfoFound = true;
0106                     XMLEle *subEP = nullptr;
0107                     for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
0108                     {
0109                         const char *subTag = tagXMLEle(subEP);
0110                         if (!strcmp(subTag, "Target"))
0111                             setTargetName(pcdataXMLEle(subEP));
0112                         else if (!strcmp(subTag, "Group"))
0113                             setGroup(pcdataXMLEle(subEP));
0114                         else if (!strcmp(subTag, "FinishSequence"))
0115                             setCompletionCondition(subTag);
0116                         else if (!strcmp(subTag, "FinishRepeat"))
0117                             setCompletionCondition(subTag, pcdataXMLEle(subEP));
0118                         else if (!strcmp(subTag, "FinishSLoop"))
0119                             setCompletionCondition(subTag);
0120                         else if (!strcmp(subTag, "Sequence"))
0121                             setSequenceFile(pcdataXMLEle(subEP));
0122                         else if (!strcmp(subTag, "Directory"))
0123                             setOutputDirectory(pcdataXMLEle(subEP));
0124                         else if (!strcmp(subTag,  "FocusEveryN"))
0125                             setFocusEveryN(cLocale.toInt(pcdataXMLEle(subEP)));
0126                         else if (!strcmp(subTag, "AlignEveryN"))
0127                             setAlignEveryN(cLocale.toInt(pcdataXMLEle(subEP)));
0128                         else if (!strcmp(subTag,  "TrackChecked"))
0129                             m_TrackChecked = true;
0130                         else if (!strcmp(subTag,  "FocusChecked"))
0131                             m_FocusChecked = true;
0132                         else if (!strcmp(subTag, "AlignChecked"))
0133                             m_AlignChecked = true;
0134                         else if (!strcmp(subTag, "GuideChecked"))
0135                             m_GuideChecked = true;
0136                         else if (!strcmp(subTag, "Overlap"))
0137                             setOverlap(cLocale.toDouble(pcdataXMLEle(subEP)));
0138                         else if (!strcmp(subTag, "CenterRA"))
0139                         {
0140                             dms ra;
0141                             ra.setH(cLocale.toDouble(pcdataXMLEle(subEP)));
0142                             setRA0(ra);
0143                         }
0144                         else if (!strcmp(subTag, "CenterDE"))
0145                         {
0146                             dms de;
0147                             de.setD(cLocale.toDouble(pcdataXMLEle(subEP)));
0148                             setDec0(de);
0149                         }
0150                         else if (!strcmp(subTag, "GridW"))
0151                             m_GridSize.setWidth(cLocale.toInt(pcdataXMLEle(subEP)));
0152                         else if (!strcmp(subTag, "GridH"))
0153                             m_GridSize.setHeight(cLocale.toInt(pcdataXMLEle(subEP)));
0154                         else if (!strcmp(subTag, "FOVW"))
0155                             m_MosaicFOV.setWidth(cLocale.toDouble(pcdataXMLEle(subEP)));
0156                         else if (!strcmp(subTag, "FOVH"))
0157                             m_MosaicFOV.setHeight(cLocale.toDouble(pcdataXMLEle(subEP)));
0158                         else if (!strcmp(subTag, "CameraFOVW"))
0159                             m_CameraFOV.setWidth(cLocale.toDouble(pcdataXMLEle(subEP)));
0160                         else if (!strcmp(subTag, "CameraFOVH"))
0161                             m_CameraFOV.setHeight(cLocale.toDouble(pcdataXMLEle(subEP)));
0162                     }
0163                 }
0164                 else if (mosaicInfoFound && !strcmp(tag, "Job"))
0165                     processJobInfo(ep, index++);
0166             }
0167             delXMLEle(root);
0168         }
0169         else if (errmsg[0])
0170         {
0171             delLilXML(xmlParser);
0172             return false;
0173         }
0174     }
0175 
0176     delLilXML(xmlParser);
0177     if (mosaicInfoFound)
0178         updateCoordsNow(KStarsData::Instance()->updateNum());
0179     return mosaicInfoFound;
0180 }
0181 
0182 bool MosaicTiles::processJobInfo(XMLEle *root, int index)
0183 {
0184     XMLEle *ep;
0185     XMLEle *subEP;
0186 
0187     // We expect all data read from the XML to be in the C locale - QLocale::c()
0188     QLocale cLocale = QLocale::c();
0189 
0190     OneTile newTile;
0191     for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
0192     {
0193         newTile.index = index;
0194         if (!strcmp(tagXMLEle(ep), "Coordinates"))
0195         {
0196             subEP = findXMLEle(ep, "J2000RA");
0197             if (subEP)
0198             {
0199                 dms ra;
0200                 ra.setH(cLocale.toDouble(pcdataXMLEle(subEP)));
0201                 newTile.skyCenter.setRA0(ra);
0202             }
0203             subEP = findXMLEle(ep, "J2000DE");
0204             if (subEP)
0205             {
0206                 dms de;
0207                 de.setD(cLocale.toDouble(pcdataXMLEle(subEP)));
0208                 newTile.skyCenter.setDec0(de);
0209             }
0210         }
0211         else if (!strcmp(tagXMLEle(ep), "TileCenter"))
0212         {
0213             if ((subEP = findXMLEle(ep, "X")))
0214                 newTile.center.setX(cLocale.toDouble(pcdataXMLEle(subEP)));
0215             if ((subEP = findXMLEle(ep, "Y")))
0216                 newTile.center.setY(cLocale.toDouble(pcdataXMLEle(subEP)));
0217             if ((subEP = findXMLEle(ep, "Rotation")))
0218                 newTile.rotation = cLocale.toDouble(pcdataXMLEle(subEP));
0219         }
0220         else if (!strcmp(tagXMLEle(ep), "PositionAngle"))
0221         {
0222             m_PositionAngle = cLocale.toDouble(pcdataXMLEle(ep));
0223         }
0224     }
0225 
0226     newTile.skyCenter.updateCoordsNow(KStarsData::Instance()->updateNum());
0227     appendTile(newTile);
0228     return true;
0229 }
0230 
0231 
0232 //bool MosaicTiles::toJSON(QJsonObject &output)
0233 //{
0234 //    Q_UNUSED(output)
0235 //    return false;
0236 //}
0237 
0238 //bool MosaicTiles::fromJSON(const QJsonObject &input)
0239 //{
0240 //    Q_UNUSED(input)
0241 //    return false;
0242 //}
0243 
0244 void MosaicTiles::appendTile(const OneTile &value)
0245 {
0246     m_Tiles.append(std::make_shared<OneTile>(value));
0247 }
0248 
0249 void MosaicTiles::appendEmptyTile()
0250 {
0251     m_Tiles.append(std::make_shared<OneTile>());
0252 }
0253 
0254 void MosaicTiles::clearTiles()
0255 {
0256     m_Tiles.clear();
0257 }
0258 
0259 QSizeF MosaicTiles::adjustCoordinate(QPointF tileCoord)
0260 {
0261     // Compute the declination of the tile row from the mosaic center
0262     double const dec = dec0().Degrees() + tileCoord.y() / 60.0;
0263 
0264     // Adjust RA based on the shift in declination
0265     QSizeF const toSpherical(1 / cos(dec * dms::DegToRad), 1);
0266 
0267     // Return the adjusted coordinates as a QSizeF in degrees
0268     return QSizeF(tileCoord.x() / 60.0 * toSpherical.width(), tileCoord.y() / 60.0 * toSpherical.height());
0269 }
0270 
0271 void MosaicTiles::createTiles(bool s_shaped)
0272 {
0273     m_SShaped = s_shaped;
0274     updateTiles();
0275 }
0276 
0277 void MosaicTiles::updateTiles()
0278 {
0279     // Sky map has objects moving from left to right, so configure the mosaic from right to left, column per column
0280     const auto fovW = m_CameraFOV.width();
0281     const auto fovH = m_CameraFOV.height();
0282     const auto gridW = m_GridSize.width();
0283     const auto gridH = m_GridSize.height();
0284 
0285     // Offset is our tile size with an overlap removed
0286     double const xOffset = fovW * (1 - m_Overlap / 100.0);
0287     double const yOffset = fovH * (1 - m_Overlap / 100.0);
0288 
0289     // We start at top right corner, (0,0) being the center of the tileset
0290     double initX = +(fovW + xOffset * (gridW - 1)) / 2.0 - fovW;
0291     double initY = -(fovH + yOffset * (gridH - 1)) / 2.0;
0292 
0293     double x = initX, y = initY;
0294 
0295     //    qCDebug(KSTARS_EKOS_SCHEDULER) << "Mosaic Tile FovW" << fovW << "FovH" << fovH << "initX" << x << "initY" << y <<
0296     //                                   "Offset X " << xOffset << " Y " << yOffset << " rotation " << pa << " reverseOdd " << s_shaped;
0297 
0298     // Start by clearing existing tiles.
0299     clearTiles();
0300 
0301     int index = 0;
0302     for (int col = 0; col < gridW; col++)
0303     {
0304         y = (m_SShaped && (col % 2)) ? (y - yOffset) : initY;
0305 
0306         for (int row = 0; row < gridH; row++)
0307         {
0308             QPointF pos(x, y);
0309             QPointF tile_center(pos.x() + (fovW / 2.0), pos.y() + (fovH / 2.0));
0310 
0311             // The location of the tile on the sky map refers to the center of the mosaic, and rotates with the mosaic itself
0312             const auto tileSkyLocation = QPointF(0, 0) - rotatePoint(tile_center, QPointF(), m_PositionAngle);
0313 
0314             // Compute the adjusted location in RA/DEC
0315             const auto tileSkyOffsetScaled = adjustCoordinate(tileSkyLocation);
0316 
0317             auto adjusted_ra0 = (ra0().Degrees() + tileSkyOffsetScaled.width()) / 15.0;
0318             auto adjusted_de0 = (dec0().Degrees() + tileSkyOffsetScaled.height());
0319             SkyPoint sky_center(adjusted_ra0, adjusted_de0);
0320             sky_center.apparentCoord(static_cast<long double>(J2000), KStarsData::Instance()->ut().djd());
0321 
0322             auto tile_center_ra0 = sky_center.ra0().Degrees();
0323             auto mosaic_center_ra0 = ra0().Degrees();
0324             auto rotation =  tile_center_ra0 - mosaic_center_ra0;
0325 
0326             // Large rotations handled wrong by the algorithm - prefer doing multiple mosaics
0327             if (abs(rotation) <= 90.0)
0328             {
0329                 auto next_index = ++index;
0330                 MosaicTiles::OneTile tile = {pos, tile_center, sky_center, rotation, next_index};
0331                 appendTile(tile);
0332             }
0333             else
0334             {
0335                 appendEmptyTile();
0336             }
0337 
0338             y += (m_SShaped && (col % 2)) ? -yOffset : +yOffset;
0339         }
0340 
0341         x -= xOffset;
0342     }
0343 }
0344 
0345 void MosaicTiles::draw(QPainter *painter)
0346 {
0347     if (m_Tiles.size() == 0)
0348         return;
0349 
0350     auto pixelScale = Options::zoomFactor() * dms::DegToRad / 60.0;
0351     const auto fovW = m_CameraFOV.width() * pixelScale;
0352     const auto fovH = m_CameraFOV.height() * pixelScale;
0353     const auto mosaicFOVW = m_MosaicFOV.width() * pixelScale;
0354     const auto mosaicFOVH = m_MosaicFOV.height() * pixelScale;
0355     const auto gridW = m_GridSize.width();
0356     const auto gridH = m_GridSize.height();
0357 
0358     QFont defaultFont = painter->font();
0359     QRect const oneRect(-fovW / 2, -fovH / 2, fovW, fovH);
0360 
0361     auto alphaValue = m_PainterAlpha;
0362 
0363     if (m_PainterAlphaAuto)
0364     {
0365         // Tiles should be more transparent when many are overlapped
0366         // Overlap < 50%: low transparency, as only two tiles will overlap on a line
0367         // 50% < Overlap < 75%: mid transparency, as three tiles will overlap one a line
0368         // 75% < Overlap: high transparency, as four tiles will overlap on a line
0369         // Slider controlling transparency provides [5%,50%], which is scaled to 0-200 alpha.
0370 
0371         if (m_Tiles.size() > 1)
0372             alphaValue = (40 - m_Overlap / 2);
0373         else
0374             alphaValue = 40;
0375     }
0376 
0377     // Draw a light background field first to help detect holes - reduce alpha as we are stacking tiles over this
0378     painter->setBrush(QBrush(QColor(255, 0, 0, (200 * alphaValue) / 100), Qt::SolidPattern));
0379     painter->setPen(QPen(painter->brush(), 2, Qt::PenStyle::DotLine));
0380     painter->drawRect(QRectF(QPointF(-mosaicFOVW / 2, -mosaicFOVH / 2), QSizeF(mosaicFOVW, mosaicFOVH)));
0381 
0382     // Fill tiles with a transparent brush to show overlaps
0383     QBrush tileBrush(QColor(0, 255, 0, (200 * alphaValue) / 100), Qt::SolidPattern);
0384 
0385     // Draw each tile, adjusted for rotation
0386     for (int row = 0; row < gridH; row++)
0387     {
0388         for (int col = 0; col < gridW; col++)
0389         {
0390             auto tile = oneTile(row, col);
0391             if (tile)
0392             {
0393                 painter->save();
0394 
0395                 painter->translate(tile->center * pixelScale);
0396                 painter->rotate(tile->rotation);
0397 
0398                 painter->setBrush(tileBrush);
0399                 painter->setPen(m_Pen);
0400 
0401                 painter->drawRect(oneRect);
0402 
0403                 painter->restore();
0404             }
0405         }
0406     }
0407 
0408     // Overwrite with tile information
0409     for (int row = 0; row < gridH; row++)
0410     {
0411         for (int col = 0; col < gridW; col++)
0412         {
0413             auto tile = oneTile(row, col);
0414             if (tile)
0415             {
0416                 painter->save();
0417 
0418                 painter->translate(tile->center * pixelScale);
0419                 // Add 180 to match Position Angle per the standard definition
0420                 // when camera image is read bottom-up instead of KStars standard top-bottom.
0421                 //painter->rotate(tile->rotation + 180);
0422 
0423                 painter->rotate(tile->rotation);
0424 
0425                 painter->setBrush(m_TextBrush);
0426                 painter->setPen(m_TextPen);
0427 
0428                 defaultFont.setPointSize(qMax(1., 4 * pixelScale * m_CameraFOV.width() / 60.));
0429                 painter->setFont(defaultFont);
0430                 painter->drawText(oneRect, Qt::AlignRight | Qt::AlignTop, QString("%1.").arg(tile->index));
0431 
0432                 defaultFont.setPointSize(qMax(1., 4 * pixelScale * m_CameraFOV.width() / 60.));
0433                 painter->setFont(defaultFont);
0434                 painter->drawText(oneRect, Qt::AlignHCenter | Qt::AlignVCenter, QString("%1\n%2")
0435                                   .arg(tile->skyCenter.ra0().toHMSString(), tile->skyCenter.dec0().toDMSString()));
0436                 painter->drawText(oneRect, Qt::AlignHCenter | Qt::AlignBottom, QString("%1%2°")
0437                                   .arg(tile->rotation >= 0.01 ? '+' : tile->rotation <= -0.01 ? '-' : '~')
0438                                   .arg(abs(tile->rotation), 5, 'f', 2));
0439 
0440                 painter->restore();
0441             }
0442         }
0443     }
0444 }
0445 
0446 QPointF MosaicTiles::rotatePoint(QPointF pointToRotate, QPointF centerPoint, double paDegrees)
0447 {
0448     if (paDegrees < 0)
0449         paDegrees += 360;
0450     double angleInRadians = -paDegrees * dms::DegToRad;
0451     double cosTheta       = cos(angleInRadians);
0452     double sinTheta       = sin(angleInRadians);
0453 
0454     QPointF rotation_point;
0455 
0456     rotation_point.setX((cosTheta * (pointToRotate.x() - centerPoint.x()) -
0457                          sinTheta * (pointToRotate.y() - centerPoint.y()) + centerPoint.x()));
0458     rotation_point.setY((sinTheta * (pointToRotate.x() - centerPoint.x()) +
0459                          cosTheta * (pointToRotate.y() - centerPoint.y()) + centerPoint.y()));
0460 
0461     return rotation_point;
0462 }
0463 
0464 QSizeF MosaicTiles::calculateTargetMosaicFOV() const
0465 {
0466     const auto xFOV = m_CameraFOV.width() * (1 - m_Overlap / 100.0);
0467     const auto yFOV = m_CameraFOV.height() * (1 - m_Overlap / 100.0);
0468     return QSizeF(xFOV, yFOV);
0469 }
0470 
0471 QSize MosaicTiles::mosaicFOVToGrid() const
0472 {
0473     // Else we get one tile, plus as many overlapping camera FOVs in the remnant of the target FOV
0474     const auto xFOV = m_CameraFOV.width() * (1 - m_Overlap / 100.0);
0475     const auto yFOV = m_CameraFOV.height() * (1 - m_Overlap / 100.0);
0476     const auto xTiles = 1 + ceil((m_MosaicFOV.width() - m_CameraFOV.width()) / xFOV);
0477     const auto yTiles = 1 + ceil((m_MosaicFOV.height() - m_CameraFOV.height()) / yFOV);
0478     return QSize(xTiles, yTiles);
0479 }
0480 
0481 QSizeF MosaicTiles::calculateCameraFOV() const
0482 {
0483     auto reducedFocalLength = m_FocalLength * m_FocalReducer;
0484     // Calculate FOV in arcmins
0485     double const fov_x =
0486         206264.8062470963552 * m_CameraSize.width() * m_PixelSize.width() / 60000.0 / reducedFocalLength;
0487     double const fov_y =
0488         206264.8062470963552 * m_CameraSize.height() * m_PixelSize.height() / 60000.0 / reducedFocalLength;
0489     return QSizeF(fov_x, fov_y);
0490 }
0491 
0492 void MosaicTiles::syncFOVs()
0493 {
0494     m_CameraFOV = calculateCameraFOV();
0495     m_MosaicFOV = calculateTargetMosaicFOV();
0496 }
0497