Kstars

mosaictiles.cpp
1/*
2 SPDX-FileCopyrightText: 2022 Jasem Mutlaq <mutlaqja@ikarustech.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include <QPainter>
8
9#include "mosaictiles.h"
10#include "kstarsdata.h"
11#include "Options.h"
12
13MosaicTiles::MosaicTiles() : SkyObject()
14{
15 setName(QLatin1String("Mosaic Tiles"));
16
17 m_Brush.setStyle(Qt::NoBrush);
18 m_Pen.setColor(QColor(200, 200, 200, 100));
19 m_Pen.setWidth(1);
20
21 m_TextBrush.setStyle(Qt::SolidPattern);
22 m_TextPen.setColor(Qt::red);
23 m_TextPen.setWidth(2);
24
25 m_FocalLength = Options::telescopeFocalLength();
26 m_FocalReducer = Options::telescopeFocalReducer();
27 m_CameraSize.setWidth(Options::cameraWidth());
28 m_CameraSize.setHeight(Options::cameraHeight());
29 m_PixelSize.setWidth(Options::cameraPixelWidth());
30 m_PixelSize.setHeight(Options::cameraPixelHeight());
31 m_PositionAngle = Options::cameraRotation();
32
33 // Initially for 1x1 Grid
34 m_MosaicFOV = m_CameraFOV = calculateCameraFOV();
35
36 createTiles(false);
37}
38
39MosaicTiles::~MosaicTiles()
40{
41}
42
43bool MosaicTiles::isValid() const
44{
45 return m_MosaicFOV.width() > 0 && m_MosaicFOV.height() > 0;
46}
47
48void MosaicTiles::setPositionAngle(double value)
49{
50 m_PositionAngle = value;
51}
52
53void MosaicTiles::setOverlap(double value)
54{
55 m_Overlap = (value < 0) ? 0 : (value > 100) ? 100 : value;
56}
57
58std::shared_ptr<MosaicTiles::OneTile> MosaicTiles::oneTile(int row, int col)
59{
60 int offset = row * m_GridSize.width() + col;
61
62 if (offset < 0 || offset >= m_Tiles.size())
63 return nullptr;
64
65 return m_Tiles[offset];
66}
67
68bool MosaicTiles::fromXML(const QString &filename)
69{
70 QFile sFile;
71 sFile.setFileName(filename);
72
73 if (!sFile.open(QIODevice::ReadOnly))
74 return false;
75
76 LilXML *xmlParser = newLilXML();
77 char errmsg[2048] = {0};
78 XMLEle *root = nullptr;
79 XMLEle *ep = nullptr;
80 char c;
81
82 m_OperationMode = MODE_OPERATION;
83
84 m_Tiles.clear();
85
86 // We expect all data read from the XML to be in the C locale - QLocale::c()
87 QLocale cLocale = QLocale::c();
88
89 bool mosaicInfoFound = false;
90 int index = 1;
91
92 m_TrackChecked = m_FocusChecked = m_AlignChecked = m_GuideChecked = false;
93
94 while (sFile.getChar(&c))
95 {
96 root = readXMLEle(xmlParser, c, errmsg);
97
98 if (root)
99 {
100 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
101 {
102 const char *tag = tagXMLEle(ep);
103 if (!strcmp(tag, "Mosaic"))
104 {
105 mosaicInfoFound = true;
106 XMLEle *subEP = nullptr;
107 for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
108 {
109 const char *subTag = tagXMLEle(subEP);
110 if (!strcmp(subTag, "Target"))
111 setTargetName(pcdataXMLEle(subEP));
112 else if (!strcmp(subTag, "Group"))
113 setGroup(pcdataXMLEle(subEP));
114 else if (!strcmp(subTag, "FinishSequence"))
115 setCompletionCondition(subTag);
116 else if (!strcmp(subTag, "FinishRepeat"))
117 setCompletionCondition(subTag, pcdataXMLEle(subEP));
118 else if (!strcmp(subTag, "FinishSLoop"))
119 setCompletionCondition(subTag);
120 else if (!strcmp(subTag, "Sequence"))
121 setSequenceFile(pcdataXMLEle(subEP));
122 else if (!strcmp(subTag, "Directory"))
123 setOutputDirectory(pcdataXMLEle(subEP));
124 else if (!strcmp(subTag, "FocusEveryN"))
125 setFocusEveryN(cLocale.toInt(pcdataXMLEle(subEP)));
126 else if (!strcmp(subTag, "AlignEveryN"))
127 setAlignEveryN(cLocale.toInt(pcdataXMLEle(subEP)));
128 else if (!strcmp(subTag, "TrackChecked"))
129 m_TrackChecked = true;
130 else if (!strcmp(subTag, "FocusChecked"))
131 m_FocusChecked = true;
132 else if (!strcmp(subTag, "AlignChecked"))
133 m_AlignChecked = true;
134 else if (!strcmp(subTag, "GuideChecked"))
135 m_GuideChecked = true;
136 else if (!strcmp(subTag, "Overlap"))
137 setOverlap(cLocale.toDouble(pcdataXMLEle(subEP)));
138 else if (!strcmp(subTag, "CenterRA"))
139 {
140 dms ra;
141 ra.setH(cLocale.toDouble(pcdataXMLEle(subEP)));
142 setRA0(ra);
143 }
144 else if (!strcmp(subTag, "CenterDE"))
145 {
146 dms de;
147 de.setD(cLocale.toDouble(pcdataXMLEle(subEP)));
148 setDec0(de);
149 }
150 else if (!strcmp(subTag, "GridW"))
151 m_GridSize.setWidth(cLocale.toInt(pcdataXMLEle(subEP)));
152 else if (!strcmp(subTag, "GridH"))
153 m_GridSize.setHeight(cLocale.toInt(pcdataXMLEle(subEP)));
154 else if (!strcmp(subTag, "FOVW"))
155 m_MosaicFOV.setWidth(cLocale.toDouble(pcdataXMLEle(subEP)));
156 else if (!strcmp(subTag, "FOVH"))
157 m_MosaicFOV.setHeight(cLocale.toDouble(pcdataXMLEle(subEP)));
158 else if (!strcmp(subTag, "CameraFOVW"))
159 m_CameraFOV.setWidth(cLocale.toDouble(pcdataXMLEle(subEP)));
160 else if (!strcmp(subTag, "CameraFOVH"))
161 m_CameraFOV.setHeight(cLocale.toDouble(pcdataXMLEle(subEP)));
162 }
163 }
164 else if (mosaicInfoFound && !strcmp(tag, "Job"))
165 processJobInfo(ep, index++);
166 }
167 delXMLEle(root);
168 }
169 else if (errmsg[0])
170 {
171 delLilXML(xmlParser);
172 return false;
173 }
174 }
175
176 delLilXML(xmlParser);
177 if (mosaicInfoFound)
178 updateCoordsNow(KStarsData::Instance()->updateNum());
179 return mosaicInfoFound;
180}
181
182bool MosaicTiles::processJobInfo(XMLEle *root, int index)
183{
184 XMLEle *ep;
185 XMLEle *subEP;
186
187 // We expect all data read from the XML to be in the C locale - QLocale::c()
188 QLocale cLocale = QLocale::c();
189
190 OneTile newTile;
191 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
192 {
193 newTile.index = index;
194 if (!strcmp(tagXMLEle(ep), "Coordinates"))
195 {
196 subEP = findXMLEle(ep, "J2000RA");
197 if (subEP)
198 {
199 dms ra;
200 ra.setH(cLocale.toDouble(pcdataXMLEle(subEP)));
201 newTile.skyCenter.setRA0(ra);
202 }
203 subEP = findXMLEle(ep, "J2000DE");
204 if (subEP)
205 {
206 dms de;
207 de.setD(cLocale.toDouble(pcdataXMLEle(subEP)));
208 newTile.skyCenter.setDec0(de);
209 }
210 }
211 else if (!strcmp(tagXMLEle(ep), "TileCenter"))
212 {
213 if ((subEP = findXMLEle(ep, "X")))
214 newTile.center.setX(cLocale.toDouble(pcdataXMLEle(subEP)));
215 if ((subEP = findXMLEle(ep, "Y")))
216 newTile.center.setY(cLocale.toDouble(pcdataXMLEle(subEP)));
217 if ((subEP = findXMLEle(ep, "Rotation")))
218 newTile.rotation = cLocale.toDouble(pcdataXMLEle(subEP));
219 }
220 else if (!strcmp(tagXMLEle(ep), "PositionAngle"))
221 {
222 m_PositionAngle = cLocale.toDouble(pcdataXMLEle(ep));
223 }
224 }
225
226 newTile.skyCenter.updateCoordsNow(KStarsData::Instance()->updateNum());
227 appendTile(newTile);
228 return true;
229}
230
231
232//bool MosaicTiles::toJSON(QJsonObject &output)
233//{
234// Q_UNUSED(output)
235// return false;
236//}
237
238//bool MosaicTiles::fromJSON(const QJsonObject &input)
239//{
240// Q_UNUSED(input)
241// return false;
242//}
243
244void MosaicTiles::appendTile(const OneTile &value)
245{
246 m_Tiles.append(std::make_shared<OneTile>(value));
247}
248
249void MosaicTiles::appendEmptyTile()
250{
251 m_Tiles.append(std::make_shared<OneTile>());
252}
253
254void MosaicTiles::clearTiles()
255{
256 m_Tiles.clear();
257}
258
259QSizeF MosaicTiles::adjustCoordinate(QPointF tileCoord)
260{
261 // Compute the declination of the tile row from the mosaic center
262 double const dec = dec0().Degrees() + tileCoord.y() / 60.0;
263
264 // Adjust RA based on the shift in declination
265 QSizeF const toSpherical(1 / cos(dec * dms::DegToRad), 1);
266
267 // Return the adjusted coordinates as a QSizeF in degrees
268 return QSizeF(tileCoord.x() / 60.0 * toSpherical.width(), tileCoord.y() / 60.0 * toSpherical.height());
269}
270
271void MosaicTiles::createTiles(bool s_shaped)
272{
273 m_SShaped = s_shaped;
274 updateTiles();
275}
276
277void MosaicTiles::updateTiles()
278{
279 // Sky map has objects moving from left to right, so configure the mosaic from right to left, column per column
280 const auto fovW = m_CameraFOV.width();
281 const auto fovH = m_CameraFOV.height();
282 const auto gridW = m_GridSize.width();
283 const auto gridH = m_GridSize.height();
284
285 // Offset is our tile size with an overlap removed
286 double const xOffset = fovW * (1 - m_Overlap / 100.0);
287 double const yOffset = fovH * (1 - m_Overlap / 100.0);
288
289 // We start at top right corner, (0,0) being the center of the tileset
290 double initX = +(fovW + xOffset * (gridW - 1)) / 2.0 - fovW;
291 double initY = -(fovH + yOffset * (gridH - 1)) / 2.0;
292
293 double x = initX, y = initY;
294
295 // qCDebug(KSTARS_EKOS_SCHEDULER) << "Mosaic Tile FovW" << fovW << "FovH" << fovH << "initX" << x << "initY" << y <<
296 // "Offset X " << xOffset << " Y " << yOffset << " rotation " << pa << " reverseOdd " << s_shaped;
297
298 // Start by clearing existing tiles.
299 clearTiles();
300
301 int index = 0;
302 for (int col = 0; col < gridW; col++)
303 {
304 y = (m_SShaped && (col % 2)) ? (y - yOffset) : initY;
305
306 for (int row = 0; row < gridH; row++)
307 {
308 QPointF pos(x, y);
309 QPointF tile_center(pos.x() + (fovW / 2.0), pos.y() + (fovH / 2.0));
310
311 // The location of the tile on the sky map refers to the center of the mosaic, and rotates with the mosaic itself
312 const auto tileSkyLocation = QPointF(0, 0) - rotatePoint(tile_center, QPointF(), m_PositionAngle);
313
314 // Compute the adjusted location in RA/DEC
315 const auto tileSkyOffsetScaled = adjustCoordinate(tileSkyLocation);
316
317 auto adjusted_ra0 = (ra0().Degrees() + tileSkyOffsetScaled.width()) / 15.0;
318 auto adjusted_de0 = (dec0().Degrees() + tileSkyOffsetScaled.height());
319 SkyPoint sky_center(adjusted_ra0, adjusted_de0);
320 sky_center.apparentCoord(static_cast<long double>(J2000), KStarsData::Instance()->ut().djd());
321
322 auto tile_center_ra0 = sky_center.ra0().Degrees();
323 auto mosaic_center_ra0 = ra0().Degrees();
324 auto rotation = tile_center_ra0 - mosaic_center_ra0;
325
326 // Large rotations handled wrong by the algorithm - prefer doing multiple mosaics
327 if (abs(rotation) <= 90.0)
328 {
329 auto next_index = ++index;
330 MosaicTiles::OneTile tile = {pos, tile_center, sky_center, rotation, next_index};
331 appendTile(tile);
332 }
333 else
334 {
335 appendEmptyTile();
336 }
337
338 y += (m_SShaped && (col % 2)) ? -yOffset : +yOffset;
339 }
340
341 x -= xOffset;
342 }
343}
344
345void MosaicTiles::draw(QPainter *painter)
346{
347 if (m_Tiles.size() == 0)
348 return;
349
350 auto pixelScale = Options::zoomFactor() * dms::DegToRad / 60.0;
351 const auto fovW = m_CameraFOV.width() * pixelScale;
352 const auto fovH = m_CameraFOV.height() * pixelScale;
353 const auto mosaicFOVW = m_MosaicFOV.width() * pixelScale;
354 const auto mosaicFOVH = m_MosaicFOV.height() * pixelScale;
355 const auto gridW = m_GridSize.width();
356 const auto gridH = m_GridSize.height();
357
358 QFont defaultFont = painter->font();
359 QRect const oneRect(-fovW / 2, -fovH / 2, fovW, fovH);
360
361 auto alphaValue = m_PainterAlpha;
362
363 if (m_PainterAlphaAuto)
364 {
365 // Tiles should be more transparent when many are overlapped
366 // Overlap < 50%: low transparency, as only two tiles will overlap on a line
367 // 50% < Overlap < 75%: mid transparency, as three tiles will overlap one a line
368 // 75% < Overlap: high transparency, as four tiles will overlap on a line
369 // Slider controlling transparency provides [5%,50%], which is scaled to 0-200 alpha.
370
371 if (m_Tiles.size() > 1)
372 alphaValue = (40 - m_Overlap / 2);
373 else
374 alphaValue = 40;
375 }
376
377 // Draw a light background field first to help detect holes - reduce alpha as we are stacking tiles over this
378 painter->setBrush(QBrush(QColor(255, 0, 0, (200 * alphaValue) / 100), Qt::SolidPattern));
379 painter->setPen(QPen(painter->brush(), 2, Qt::PenStyle::DotLine));
380 painter->drawRect(QRectF(QPointF(-mosaicFOVW / 2, -mosaicFOVH / 2), QSizeF(mosaicFOVW, mosaicFOVH)));
381
382 // Fill tiles with a transparent brush to show overlaps
383 QBrush tileBrush(QColor(0, 255, 0, (200 * alphaValue) / 100), Qt::SolidPattern);
384
385 // Draw each tile, adjusted for rotation
386 for (int row = 0; row < gridH; row++)
387 {
388 for (int col = 0; col < gridW; col++)
389 {
390 auto tile = oneTile(row, col);
391 if (tile)
392 {
393 painter->save();
394
395 painter->translate(tile->center * pixelScale);
396 painter->rotate(tile->rotation);
397
398 painter->setBrush(tileBrush);
399 painter->setPen(m_Pen);
400
401 painter->drawRect(oneRect);
402
403 painter->restore();
404 }
405 }
406 }
407
408 // Overwrite with tile information
409 for (int row = 0; row < gridH; row++)
410 {
411 for (int col = 0; col < gridW; col++)
412 {
413 auto tile = oneTile(row, col);
414 if (tile)
415 {
416 painter->save();
417
418 painter->translate(tile->center * pixelScale);
419 // Add 180 to match Position Angle per the standard definition
420 // when camera image is read bottom-up instead of KStars standard top-bottom.
421 //painter->rotate(tile->rotation + 180);
422
423 painter->rotate(tile->rotation);
424
425 painter->setBrush(m_TextBrush);
426 painter->setPen(m_TextPen);
427
428 defaultFont.setPointSize(qMax(1., 4 * pixelScale * m_CameraFOV.width() / 60.));
429 painter->setFont(defaultFont);
430 painter->drawText(oneRect, Qt::AlignRight | Qt::AlignTop, QString("%1.").arg(tile->index));
431
432 defaultFont.setPointSize(qMax(1., 4 * pixelScale * m_CameraFOV.width() / 60.));
433 painter->setFont(defaultFont);
434 painter->drawText(oneRect, Qt::AlignHCenter | Qt::AlignVCenter, QString("%1\n%2")
435 .arg(tile->skyCenter.ra0().toHMSString(), tile->skyCenter.dec0().toDMSString()));
436 painter->drawText(oneRect, Qt::AlignHCenter | Qt::AlignBottom, QString("%1%2°")
437 .arg(tile->rotation >= 0.01 ? '+' : tile->rotation <= -0.01 ? '-' : '~')
438 .arg(abs(tile->rotation), 5, 'f', 2));
439
440 painter->restore();
441 }
442 }
443 }
444}
445
446QPointF MosaicTiles::rotatePoint(QPointF pointToRotate, QPointF centerPoint, double paDegrees)
447{
448 if (paDegrees < 0)
449 paDegrees += 360;
450 double angleInRadians = -paDegrees * dms::DegToRad;
451 double cosTheta = cos(angleInRadians);
452 double sinTheta = sin(angleInRadians);
453
454 QPointF rotation_point;
455
456 rotation_point.setX((cosTheta * (pointToRotate.x() - centerPoint.x()) -
457 sinTheta * (pointToRotate.y() - centerPoint.y()) + centerPoint.x()));
458 rotation_point.setY((sinTheta * (pointToRotate.x() - centerPoint.x()) +
459 cosTheta * (pointToRotate.y() - centerPoint.y()) + centerPoint.y()));
460
461 return rotation_point;
462}
463
464QSizeF MosaicTiles::calculateTargetMosaicFOV() const
465{
466 const auto xFOV = m_CameraFOV.width() * (1 - m_Overlap / 100.0);
467 const auto yFOV = m_CameraFOV.height() * (1 - m_Overlap / 100.0);
468 return QSizeF(xFOV, yFOV);
469}
470
471QSize MosaicTiles::mosaicFOVToGrid() const
472{
473 // Else we get one tile, plus as many overlapping camera FOVs in the remnant of the target FOV
474 const auto xFOV = m_CameraFOV.width() * (1 - m_Overlap / 100.0);
475 const auto yFOV = m_CameraFOV.height() * (1 - m_Overlap / 100.0);
476 const auto xTiles = 1 + ceil((m_MosaicFOV.width() - m_CameraFOV.width()) / xFOV);
477 const auto yTiles = 1 + ceil((m_MosaicFOV.height() - m_CameraFOV.height()) / yFOV);
478 return QSize(xTiles, yTiles);
479}
480
481QSizeF MosaicTiles::calculateCameraFOV() const
482{
483 auto reducedFocalLength = m_FocalLength * m_FocalReducer;
484 // Calculate FOV in arcmins
485 double const fov_x =
486 206264.8062470963552 * m_CameraSize.width() * m_PixelSize.width() / 60000.0 / reducedFocalLength;
487 double const fov_y =
488 206264.8062470963552 * m_CameraSize.height() * m_PixelSize.height() / 60000.0 / reducedFocalLength;
489 return QSizeF(fov_x, fov_y);
490}
491
492void MosaicTiles::syncFOVs()
493{
494 m_CameraFOV = calculateCameraFOV();
495 m_MosaicFOV = calculateTargetMosaicFOV();
496}
497
void setH(const double &x) override
Sets the angle in hours, supplied as a double.
Definition cachingdms.h:91
Provides all necessary information about an object in the sky: its coordinates, name(s),...
Definition skyobject.h:42
The sky coordinates of a point in the sky.
Definition skypoint.h:45
const CachingDms & dec() const
Definition skypoint.h:269
const CachingDms & ra0() const
Definition skypoint.h:251
virtual void updateCoordsNow(const KSNumbers *num)
updateCoordsNow Shortcut for updateCoords( const KSNumbers *num, false, nullptr, nullptr,...
Definition skypoint.h:391
const CachingDms & ra() const
Definition skypoint.h:263
void setRA0(dms r)
Sets RA0, the catalog Right Ascension.
Definition skypoint.h:94
const CachingDms & dec0() const
Definition skypoint.h:257
void setDec0(dms d)
Sets Dec0, the catalog Declination.
Definition skypoint.h:119
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
virtual void setD(const double &x)
Sets floating-point value of angle, in degrees.
Definition dms.h:179
const double & Degrees() const
Definition dms.h:141
static constexpr double DegToRad
DegToRad is a const static member equal to the number of radians in one degree (dms::PI/180....
Definition dms.h:390
bool open(FILE *fh, OpenMode mode, FileHandleFlags handleFlags)
void setFileName(const QString &name)
void setPointSize(int pointSize)
bool getChar(char *c)
void append(QList< T > &&value)
void clear()
qsizetype size() const const
QLocale c()
double toDouble(QStringView s, bool *ok) const const
int toInt(QStringView s, bool *ok) const const
const QBrush & brush() const const
void drawRect(const QRect &rectangle)
void drawText(const QPoint &position, const QString &text)
const QFont & font() const const
void restore()
void rotate(qreal angle)
void save()
void setBrush(Qt::BrushStyle style)
void setFont(const QFont &font)
void setPen(Qt::PenStyle style)
void translate(const QPoint &offset)
void setX(qreal x)
void setY(qreal y)
qreal x() const const
qreal y() const const
int height() const const
void setHeight(int height)
void setWidth(int width)
int width() const const
qreal height() const const
void setHeight(qreal height)
void setWidth(qreal width)
qreal width() const const
QString arg(Args &&... args) const const
AlignRight
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 4 2024 16:38:44 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.