Kstars

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

KDE's Doxygen guidelines are available online.