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