1 /*
2     SPDX-FileCopyrightText: 2015 Jasem Mutlaq <mutlaqja@ikarustech.com>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "mosaic.h"
8 #include "ui_mosaic.h"
9 
10 #include "kstars.h"
11 #include "Options.h"
12 #include "scheduler.h"
13 #include "skymap.h"
14 #include "ekos/manager.h"
15 #include "projections/projector.h"
16 
17 #include "ekos_scheduler_debug.h"
18 
19 namespace Ekos
20 {
21 class MosaicTile : public QGraphicsItem
22 {
23     public:
24         // TODO: make this struct a QGraphicsItem
25         typedef struct
26         {
27             QPointF pos;
28             QPointF center;
29             SkyPoint skyCenter;
30             double rotation;
31             int index;
32         } OneTile;
33 
34     public:
MosaicTile()35         MosaicTile()
36         {
37             brush.setStyle(Qt::NoBrush);
38             QColor lightGray(200, 200, 200, 100);
39             pen.setColor(lightGray);
40             pen.setWidth(1);
41 
42             textBrush.setStyle(Qt::SolidPattern);
43             textPen.setColor(Qt::red);
44             textPen.setWidth(2);
45 
46             setFlags(QGraphicsItem::ItemIsMovable);
47         }
48 
~MosaicTile()49         ~MosaicTile()
50         {
51             qDeleteAll(tiles);
52         }
53 
54     public:
setSkyCenter(SkyPoint center)55         void setSkyCenter(SkyPoint center)
56         {
57             skyCenter = center;
58         }
59 
setPositionAngle(double positionAngle)60         void setPositionAngle(double positionAngle)
61         {
62             pa = std::fmod(positionAngle * -1 + 360.0, 360.0);
63 
64             // Rotate the whole mosaic around its local center
65             setTransformOriginPoint(QPointF());
66             setRotation(pa);
67         }
68 
setGridDimensions(int width,int height)69         void setGridDimensions(int width, int height)
70         {
71             w = width;
72             h = height;
73         }
74 
setSingleTileFOV(double fov_x,double fov_y)75         void setSingleTileFOV(double fov_x, double fov_y)
76         {
77             fovW = fov_x;
78             fovH = fov_y;
79         }
80 
setMosaicFOV(double mfov_x,double mfov_y)81         void setMosaicFOV(double mfov_x, double mfov_y)
82         {
83             mfovW = mfov_x;
84             mfovH = mfov_y;
85         }
86 
setOverlap(double value)87         void setOverlap(double value)
88         {
89             overlap = (value < 0) ? 0 : (1 < value) ? 1 : value;
90         }
91 
92     public:
getWidth()93         int getWidth()
94         {
95             return w;
96         }
97 
getHeight()98         int getHeight()
99         {
100             return h;
101         }
102 
getOverlap()103         double getOverlap()
104         {
105             return overlap;
106         }
107 
getPA()108         double getPA()
109         {
110             return pa;
111         }
112 
setPainterAlpha(int v)113         void setPainterAlpha(int v)
114         {
115             m_PainterAlpha = v;
116         }
117 
118     public:
119         /// @internal Returns scaled offsets for a pixel local coordinate.
120         ///
121         /// This uses the mosaic center as reference and the argument resolution of the sky map at that center.
adjustCoordinate(QPointF tileCoord,QSizeF pixPerArcsec)122         QSizeF adjustCoordinate(QPointF tileCoord, QSizeF pixPerArcsec)
123         {
124             // Compute the declination of the tile row from the mosaic center
125             double const dec = skyCenter.dec0().Degrees() + tileCoord.y() / pixPerArcsec.height();
126 
127             // Adjust RA based on the shift in declination
128             QSizeF const toSpherical(
129                         1 / (pixPerArcsec.width() * cos(dec * dms::DegToRad)),
130                         1 / (pixPerArcsec.height()));
131 
132             // Return the adjusted coordinates as a QSizeF in degrees
133             return QSizeF(tileCoord.x() * toSpherical.width(), tileCoord.y() * toSpherical.height());
134         }
135 
updateTiles(QPointF skymapCenter,QSizeF pixPerArcsec,bool s_shaped)136         void updateTiles(QPointF skymapCenter, QSizeF pixPerArcsec, bool s_shaped)
137         {
138             prepareGeometryChange();
139 
140             qDeleteAll(tiles);
141             tiles.clear();
142 
143             // Sky map has objects moving from left to right, so configure the mosaic from right to left, column per column
144 
145             // Offset is our tile size with an overlap removed
146             double const xOffset = fovW * (1 - overlap);
147             double const yOffset = fovH * (1 - overlap);
148 
149             // We start at top right corner, (0,0) being the center of the tileset
150             double initX = +(fovW + xOffset * (w - 1)) / 2.0 - fovW;
151             double initY = -(fovH + yOffset * (h - 1)) / 2.0;
152 
153             double x = initX, y = initY;
154 
155             qCDebug(KSTARS_EKOS_SCHEDULER) << "Mosaic Tile FovW" << fovW << "FovH" << fovH << "initX" << x << "initY" << y <<
156                                               "Offset X " << xOffset << " Y " << yOffset << " rotation " << getPA() << " reverseOdd " << s_shaped;
157 
158             int index = 0;
159             for (int col = 0; col < w; col++)
160             {
161                 y = (s_shaped && (col % 2)) ? (y - yOffset) : initY;
162 
163                 for (int row = 0; row < h; row++)
164                 {
165                     OneTile *tile = new OneTile();
166 
167                     if (!tile)
168                         continue;
169 
170                     tile->pos.setX(x);
171                     tile->pos.setY(y);
172 
173                     tile->center.setX(tile->pos.x() + (fovW / 2.0));
174                     tile->center.setY(tile->pos.y() + (fovH / 2.0));
175 
176                     // The location of the tile on the sky map refers to the center of the mosaic, and rotates with the mosaic itself
177                     QPointF tileSkyLocation = skymapCenter - rotatePoint(tile->center, QPointF(), rotation());
178 
179                     // Compute the adjusted location in RA/DEC
180                     QSizeF const tileSkyOffsetScaled = adjustCoordinate(tileSkyLocation, pixPerArcsec);
181 
182                     tile->skyCenter.setRA0((skyCenter.ra0().Degrees() + tileSkyOffsetScaled.width())/15.0);
183                     tile->skyCenter.setDec0(skyCenter.dec0().Degrees() + tileSkyOffsetScaled.height());
184                     tile->skyCenter.apparentCoord(static_cast<long double>(J2000), KStars::Instance()->data()->ut().djd());
185 
186                     tile->rotation = tile->skyCenter.ra0().Degrees() - skyCenter.ra0().Degrees();
187 
188                     // Large rotations handled wrong by the algorithm - prefer doing multiple mosaics
189                     if (abs(tile->rotation) <= 90.0)
190                     {
191                         tile->index = ++index;
192                         tiles.append(tile);
193                     }
194                     else
195                     {
196                         delete tile;
197                         tiles.append(nullptr);
198                     }
199 
200                     y += (s_shaped && (col % 2)) ? -yOffset : +yOffset;
201                 }
202 
203                 x -= xOffset;
204             }
205         }
206 
getTile(int row,int col)207         OneTile *getTile(int row, int col)
208         {
209             int offset = row * w + col;
210 
211             if (offset < 0 || offset >= tiles.size())
212                 return nullptr;
213 
214             return tiles[offset];
215         }
216 
boundingRect() const217         QRectF boundingRect() const override
218         {
219             double const xOffset = fovW * (1 - overlap);
220             double const yOffset = fovH * (1 - overlap);
221             return QRectF(0, 0, fovW + xOffset * (w - 1), fovH + yOffset * (h - 1));
222         }
223 
getTiles() const224         QList<OneTile *> getTiles() const
225         {
226             return tiles;
227         }
228 
229     protected:
paint(QPainter * painter,const QStyleOptionGraphicsItem *,QWidget *)230         void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override
231         {
232             if (!tiles.size())
233                 return;
234 
235             QFont defaultFont = painter->font();
236             QRect const oneRect(-fovW/2, -fovH/2, fovW, fovH);
237 
238             // HACK: all tiles should be QGraphicsItem instances so that texts would be scaled properly
239             double const fontScale = 1/log(tiles.size() < 4 ? 4 : tiles.size());
240 
241             // Draw a light background field first to help detect holes - reduce alpha as we are stacking tiles over this
242             painter->setBrush(QBrush(QColor(255, 0, 0, (200*m_PainterAlpha)/100), Qt::SolidPattern));
243             painter->setPen(QPen(painter->brush(), 2, Qt::PenStyle::DotLine));
244             painter->drawRect(QRectF(QPointF(-mfovW/2, -mfovH/2), QSizeF(mfovW, mfovH)));
245 
246             // Fill tiles with a transparent brush to show overlaps
247             QBrush tileBrush(QColor(0, 255, 0, (200*m_PainterAlpha)/100), Qt::SolidPattern);
248 
249             // Draw each tile, adjusted for rotation
250             for (int row = 0; row < h; row++)
251             {
252                 for (int col = 0; col < w; col++)
253                 {
254                     OneTile const * const tile = getTile(row, col);
255                     if (tile)
256                     {
257                         QTransform const transform = painter->worldTransform();
258                         painter->translate(tile->center);
259                         painter->rotate(tile->rotation);
260 
261                         painter->setBrush(tileBrush);
262                         painter->setPen(pen);
263 
264                         painter->drawRect(oneRect);
265 
266                         painter->setWorldTransform(transform);
267                     }
268                 }
269             }
270 
271             // Overwrite with tile information
272             for (int row = 0; row < h; row++)
273             {
274                 for (int col = 0; col < w; col++)
275                 {
276                     OneTile const * const tile = getTile(row, col);
277                     if (tile)
278                     {
279                         QTransform const transform = painter->worldTransform();
280                         painter->translate(tile->center);
281                         painter->rotate(tile->rotation);
282 
283                         painter->setBrush(textBrush);
284                         painter->setPen(textPen);
285 
286                         defaultFont.setPointSize(50*fontScale);
287                         painter->setFont(defaultFont);
288                         painter->drawText(oneRect, Qt::AlignRight | Qt::AlignTop, QString("%1.").arg(tile->index));
289 
290                         defaultFont.setPointSize(20*fontScale);
291                         painter->setFont(defaultFont);
292                         painter->drawText(oneRect, Qt::AlignHCenter | Qt::AlignVCenter, QString("%1\n%2")
293                                           .arg(tile->skyCenter.ra0().toHMSString())
294                                           .arg(tile->skyCenter.dec0().toDMSString()));
295                         painter->drawText(oneRect, Qt::AlignHCenter | Qt::AlignBottom, QString("%1%2°")
296                                           .arg(tile->rotation >= 0.01 ? '+' : tile->rotation <= -0.01 ? '-' : '~')
297                                           .arg(abs(tile->rotation), 5, 'f', 2));
298 
299                         painter->setWorldTransform(transform);
300                     }
301                 }
302             }
303         }
304 
rotatePoint(QPointF pointToRotate,QPointF centerPoint,double paDegrees)305         QPointF rotatePoint(QPointF pointToRotate, QPointF centerPoint, double paDegrees)
306         {
307             double angleInRadians = paDegrees * dms::DegToRad;
308             double cosTheta       = cos(angleInRadians);
309             double sinTheta       = sin(angleInRadians);
310 
311             QPointF rotation_point;
312 
313             rotation_point.setX((cosTheta * (pointToRotate.x() - centerPoint.x()) -
314                                  sinTheta * (pointToRotate.y() - centerPoint.y()) + centerPoint.x()));
315             rotation_point.setY((sinTheta * (pointToRotate.x() - centerPoint.x()) +
316                                  cosTheta * (pointToRotate.y() - centerPoint.y()) + centerPoint.y()));
317 
318             return rotation_point;
319         }
320 
321     private:
322         SkyPoint skyCenter;
323 
324         double overlap { 0 };
325         int w { 1 };
326         int h { 1 };
327         double fovW { 0 };
328         double fovH { 0 };
329         double mfovW { 0 };
330         double mfovH { 0 };
331         double pa { 0 };
332 
333         QBrush brush;
334         QPen pen;
335 
336         QBrush textBrush;
337         QPen textPen;
338 
339         int m_PainterAlpha { 50 };
340 
341         QList<OneTile *> tiles;
342 };
343 
Mosaic(QString targetName,SkyPoint center,QWidget * parent)344 Mosaic::Mosaic(QString targetName, SkyPoint center, QWidget *parent): QDialog(parent), ui(new Ui::mosaicDialog())
345 {
346     ui->setupUi(this);
347 
348     // Initial optics information is taken from Ekos options
349     ui->focalLenSpin->setValue(Options::telescopeFocalLength());
350     ui->pixelWSizeSpin->setValue(Options::cameraPixelWidth());
351     ui->pixelHSizeSpin->setValue(Options::cameraPixelHeight());
352     ui->cameraWSpin->setValue(Options::cameraWidth());
353     ui->cameraHSpin->setValue(Options::cameraHeight());
354     ui->rotationSpin->setValue(Options::cameraRotation());
355 
356     // Initial job location is the home path appended with the target name
357     ui->jobsDir->setText(QDir::cleanPath(QDir::homePath() + QDir::separator() + targetName.replace(' ','_')));
358     ui->selectJobsDirB->setIcon(QIcon::fromTheme("document-open-folder"));
359 
360     // The update timer avoids stacking updates which crash the sky map renderer
361     updateTimer = new QTimer(this);
362     updateTimer->setSingleShot(true);
363     updateTimer->setInterval(1000);
364     connect(updateTimer, &QTimer::timeout, this, &Ekos::Mosaic::constructMosaic);
365 
366     // Scope optics information
367     // - Changing the optics configuration changes the FOV, which changes the target field dimensions
368     connect(ui->focalLenSpin, &QDoubleSpinBox::editingFinished, this, &Ekos::Mosaic::calculateFOV);
369     connect(ui->cameraWSpin, &QSpinBox::editingFinished, this, &Ekos::Mosaic::calculateFOV);
370     connect(ui->cameraHSpin, &QSpinBox::editingFinished, this, &Ekos::Mosaic::calculateFOV);
371     connect(ui->pixelWSizeSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &Ekos::Mosaic::calculateFOV);
372     connect(ui->pixelHSizeSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &Ekos::Mosaic::calculateFOV);
373     connect(ui->rotationSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &Ekos::Mosaic::calculateFOV);
374 
375     // Mosaic configuration
376     // - Changing the target field dimensions changes the grid dimensions
377     // - Changing the overlap field changes the grid dimensions (more intuitive than changing the field)
378     // - Changing the grid dimensions changes the target field dimensions
379     connect(ui->targetHFOVSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &Ekos::Mosaic::updateGridFromTargetFOV);
380     connect(ui->targetWFOVSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &Ekos::Mosaic::updateGridFromTargetFOV);
381     connect(ui->overlapSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &Ekos::Mosaic::updateGridFromTargetFOV);
382     connect(ui->mosaicWSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &Ekos::Mosaic::updateTargetFOVFromGrid);
383     connect(ui->mosaicHSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &Ekos::Mosaic::updateTargetFOVFromGrid);
384 
385     // Lazy update for s-shape
386     connect(ui->reverseOddRows, &QCheckBox::toggled, this, [&]() { renderedHFOV = 0; updateTimer->start(); });
387 
388     // Buttons
389     connect(ui->resetB, &QPushButton::clicked, this, &Ekos::Mosaic::updateTargetFOVFromGrid);
390     connect(ui->selectJobsDirB, &QPushButton::clicked, this, &Ekos::Mosaic::saveJobsDirectory);
391     connect(ui->fetchB, &QPushButton::clicked, this, &Mosaic::fetchINDIInformation);
392 
393     // The sky map is a pixmap background, and the mosaic tiles are rendered over it
394     skyMapItem = scene.addPixmap(targetPix);
395     skyMapItem->setTransformationMode(Qt::TransformationMode::SmoothTransformation);
396     mosaicTileItem = new MosaicTile();
397     scene.addItem(mosaicTileItem);
398     ui->mosaicView->setScene(&scene);
399 
400     // Always use Equatorial Mode in Mosaic mode
401     rememberAltAzOption = Options::useAltAz();
402     Options::setUseAltAz(false);
403 
404     // Dialog can only be accepted when creating two tiles or more
405     ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
406 
407     // Rendering options
408     connect(ui->transparencySlider, QOverload<int>::of(&QSlider::valueChanged), this, [&](int v)
409     {
410         ui->transparencySlider->setToolTip(QString("%1%").arg(v));
411         mosaicTileItem->setPainterAlpha(v);
412         updateTimer->start();
413     });
414     connect(ui->transparencyAuto, &QCheckBox::toggled, this, [&](bool v)
415     {
416         emit ui->transparencySlider->setEnabled(!v);
417         if (v)
418             updateTimer->start();
419     });
420 
421     // Job options
422     connect(ui->alignEvery, QOverload<int>::of(&QSpinBox::valueChanged), this, &Ekos::Mosaic::rewordStepEvery);
423     connect(ui->focusEvery, QOverload<int>::of(&QSpinBox::valueChanged), this, &Ekos::Mosaic::rewordStepEvery);
424     emit ui->alignEvery->valueChanged(0);
425     emit ui->focusEvery->valueChanged(0);
426 
427     // Center, fetch optics and adjust size
428     setCenter(center);
429     fetchINDIInformation();
430     adjustSize();
431 }
432 
~Mosaic()433 Mosaic::~Mosaic()
434 {
435     delete updateTimer;
436     Options::setUseAltAz(rememberAltAzOption);
437 }
438 
getJobsDir() const439 QString Mosaic::getJobsDir() const
440 {
441     return ui->jobsDir->text();
442 }
443 
isScopeInfoValid() const444 bool Mosaic::isScopeInfoValid() const
445 {
446     if (0 < ui->focalLenSpin->value())
447         if (0 < ui->cameraWSpin->value() && 0 < ui->cameraWSpin->value())
448             if (0 < ui->pixelWSizeSpin->value() && 0 < ui->pixelHSizeSpin->value())
449                 return true;
450     return false;
451 }
452 
getTargetWFOV() const453 double Mosaic::getTargetWFOV() const
454 {
455     double const xFOV = ui->cameraWFOVSpin->value() * (1 - ui->overlapSpin->value()/100.0);
456     return ui->cameraWFOVSpin->value() + xFOV * (ui->mosaicWSpin->value() - 1);
457 }
458 
getTargetHFOV() const459 double Mosaic::getTargetHFOV() const
460 {
461     double const yFOV = ui->cameraHFOVSpin->value() * (1 - ui->overlapSpin->value()/100.0);
462     return ui->cameraHFOVSpin->value() + yFOV * (ui->mosaicHSpin->value() - 1);
463 }
464 
getTargetMosaicW() const465 double Mosaic::getTargetMosaicW() const
466 {
467     // If FOV is invalid, or target FOV is null, or target FOV is smaller than camera FOV, we get one tile
468     if (!isScopeInfoValid() || !ui->targetWFOVSpin->value() || ui->targetWFOVSpin->value() <= ui->cameraWFOVSpin->value())
469         return 1;
470 
471     // Else we get one tile, plus as many overlapping camera FOVs in the remnant of the target FOV
472     double const xFOV = ui->cameraWFOVSpin->value() * (1 - ui->overlapSpin->value()/100.0);
473     int const tiles = 1 + ceil((ui->targetWFOVSpin->value() - ui->cameraWFOVSpin->value()) / xFOV);
474     //Ekos::Manager::Instance()->schedulerModule()->appendLogText(QString("[W] Target FOV %1, camera FOV %2 after overlap %3, %4 tiles.").arg(ui->targetWFOVSpin->value()).arg(ui->cameraWFOVSpin->value()).arg(xFOV).arg(tiles));
475     return tiles;
476 }
477 
getTargetMosaicH() const478 double Mosaic::getTargetMosaicH() const
479 {
480     // If FOV is invalid, or target FOV is null, or target FOV is smaller than camera FOV, we get one tile
481     if (!isScopeInfoValid() || !ui->targetHFOVSpin->value() || ui->targetHFOVSpin->value() <= ui->cameraHFOVSpin->value())
482         return 1;
483 
484     // Else we get one tile, plus as many overlapping camera FOVs in the remnant of the target FOV
485     double const yFOV = ui->cameraHFOVSpin->value() * (1 - ui->overlapSpin->value()/100.0);
486     int const tiles = 1 + ceil((ui->targetHFOVSpin->value() - ui->cameraHFOVSpin->value()) / yFOV);
487     //Ekos::Manager::Instance()->schedulerModule()->appendLogText(QString("[H] Target FOV %1, camera FOV %2 after overlap %3, %4 tiles.").arg(ui->targetHFOVSpin->value()).arg(ui->cameraHFOVSpin->value()).arg(yFOV).arg(tiles));
488     return tiles;
489 }
490 
exec()491 int Mosaic::exec()
492 {
493     premosaicZoomFactor = Options::zoomFactor();
494 
495     int const result = QDialog::exec();
496 
497     // Revert various options
498     updateTimer->stop();
499     SkyMap *map = SkyMap::Instance();
500     if (map && 0 < premosaicZoomFactor)
501         map->setZoomFactor(premosaicZoomFactor);
502 
503     return result;
504 }
505 
accept()506 void Mosaic::accept()
507 {
508     //createJobs();
509     QDialog::accept();
510 }
511 
saveJobsDirectory()512 void Mosaic::saveJobsDirectory()
513 {
514     QString dir = QFileDialog::getExistingDirectory(KStars::Instance(), i18nc("@title:window", "FITS Save Directory"), ui->jobsDir->text());
515 
516     if (!dir.isEmpty())
517         ui->jobsDir->setText(dir);
518 }
519 
setCenter(const SkyPoint & value)520 void Mosaic::setCenter(const SkyPoint &value)
521 {
522     center = value;
523     center.apparentCoord(static_cast<long double>(J2000), KStars::Instance()->data()->ut().djd());
524 }
525 
setCameraSize(uint16_t width,uint16_t height)526 void Mosaic::setCameraSize(uint16_t width, uint16_t height)
527 {
528     ui->cameraWSpin->setValue(width);
529     ui->cameraHSpin->setValue(height);
530 }
531 
setPixelSize(double pixelWSize,double pixelHSize)532 void Mosaic::setPixelSize(double pixelWSize, double pixelHSize)
533 {
534     ui->pixelWSizeSpin->setValue(pixelWSize);
535     ui->pixelHSizeSpin->setValue(pixelHSize);
536 }
537 
setFocalLength(double focalLength)538 void Mosaic::setFocalLength(double focalLength)
539 {
540     ui->focalLenSpin->setValue(focalLength);
541 }
542 
calculateFOV()543 void Mosaic::calculateFOV()
544 {
545     if (!isScopeInfoValid())
546         return;
547 
548     ui->fovGroup->setEnabled(true);
549 
550     ui->targetWFOVSpin->setMinimum(ui->cameraWFOVSpin->value());
551     ui->targetHFOVSpin->setMinimum(ui->cameraHFOVSpin->value());
552 
553     Options::setTelescopeFocalLength(ui->focalLenSpin->value());
554     Options::setCameraPixelWidth(ui->pixelWSizeSpin->value());
555     Options::setCameraPixelHeight(ui->pixelHSizeSpin->value());
556     Options::setCameraWidth(ui->cameraWSpin->value());
557     Options::setCameraHeight(ui->cameraHSpin->value());
558 
559     // Calculate FOV in arcmins
560     double const fov_x =
561         206264.8062470963552 * ui->cameraWSpin->value() * ui->pixelWSizeSpin->value() / 60000.0 / ui->focalLenSpin->value();
562     double const fov_y =
563         206264.8062470963552 * ui->cameraHSpin->value() * ui->pixelHSizeSpin->value() / 60000.0 / ui->focalLenSpin->value();
564 
565     ui->cameraWFOVSpin->setValue(fov_x);
566     ui->cameraHFOVSpin->setValue(fov_y);
567 
568     double const target_fov_w = getTargetWFOV();
569     double const target_fov_h = getTargetHFOV();
570 
571     if (ui->targetWFOVSpin->value() < target_fov_w)
572     {
573         bool const sig = ui->targetWFOVSpin->blockSignals(true);
574         ui->targetWFOVSpin->setValue(target_fov_w);
575         ui->targetWFOVSpin->blockSignals(sig);
576     }
577 
578     if (ui->targetHFOVSpin->value() < target_fov_h)
579     {
580         bool const sig = ui->targetHFOVSpin->blockSignals(true);
581         ui->targetHFOVSpin->setValue(target_fov_h);
582         ui->targetHFOVSpin->blockSignals(sig);
583     }
584 
585     updateTimer->start();
586 }
587 
updateTargetFOV()588 void Mosaic::updateTargetFOV()
589 {
590     KStars *ks  = KStars::Instance();
591     SkyMap *map = SkyMap::Instance();
592 
593     // Render the required FOV
594     renderedWFOV = ui->targetWFOVSpin->value();// * cos(ui->rotationSpin->value() * dms::DegToRad);
595     renderedHFOV = ui->targetHFOVSpin->value();// * sin(ui->rotationSpin->value() * dms::DegToRad);
596 
597     // Pick thrice the largest FOV to obtain a proper zoom
598     double const spacing = ui->mosaicWSpin->value() < ui->mosaicHSpin->value() ? ui->mosaicHSpin->value() : ui->mosaicWSpin->value();
599     double const scale = 1.0 + 2.0 / (1.0 + spacing);
600     double const renderedFOV = scale * (renderedWFOV < renderedHFOV ? renderedHFOV : renderedWFOV);
601 
602     // Check the aspect ratio of the sky map, assuming the map zoom considers the width (see KStars::setApproxFOV)
603     double const aspect_ratio = map->width() / map->height();
604 
605     // Set the zoom (in degrees) that gives the expected FOV for the map aspect ratio, and center the target
606     ks->setApproxFOV(renderedFOV * aspect_ratio / 60.0);
607     //center.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
608     map->setClickedObject(nullptr);
609     map->setClickedPoint(&center);
610     map->slotCenter();
611 
612     // Wait for the map to stop slewing, so that HiPS renders properly
613     while(map->isSlewing())
614         qApp->processEvents();
615     qApp->processEvents();
616 
617     // Compute the horizontal and vertical resolutions, deduce the actual FOV of the map in arcminutes
618     pixelsPerArcminRA = pixelsPerArcminDE = Options::zoomFactor() * dms::DegToRad / 60.0;
619 
620     // Get the sky map image - don't bother subframing, it causes imprecision sometimes
621     QImage fullSkyChart(QSize(map->width(), map->height()), QImage::Format_RGB32);
622     map->exportSkyImage(&fullSkyChart, false);
623     qApp->processEvents();
624     skyMapItem->setPixmap(QPixmap::fromImage(fullSkyChart));
625 
626     // Relocate
627     QRectF sceneRect = skyMapItem->boundingRect().translated(-skyMapItem->boundingRect().center());
628     scene.setSceneRect(sceneRect);
629     skyMapItem->setOffset(sceneRect.topLeft());
630     skyMapItem->setPos(QPointF());
631 }
632 
resizeEvent(QResizeEvent *)633 void Mosaic::resizeEvent(QResizeEvent *)
634 {
635     // Adjust scene rect to avoid rounding holes on border
636     QRectF adjustedSceneRect(scene.sceneRect());
637     adjustedSceneRect.setTop(adjustedSceneRect.top()+2);
638     adjustedSceneRect.setLeft(adjustedSceneRect.left()+2);
639     adjustedSceneRect.setRight(adjustedSceneRect.right()-2);
640     adjustedSceneRect.setBottom(adjustedSceneRect.bottom()-2);
641 
642     ui->mosaicView->fitInView(adjustedSceneRect, Qt::KeepAspectRatioByExpanding);
643     ui->mosaicView->centerOn(QPointF());
644 }
645 
showEvent(QShowEvent *)646 void Mosaic::showEvent(QShowEvent *)
647 {
648     resizeEvent(nullptr);
649 }
650 
resetFOV()651 void Mosaic::resetFOV()
652 {
653     if (!isScopeInfoValid())
654         return;
655 
656     ui->targetWFOVSpin->setValue(getTargetWFOV());
657     ui->targetHFOVSpin->setValue(getTargetHFOV());
658 }
659 
updateTargetFOVFromGrid()660 void Mosaic::updateTargetFOVFromGrid()
661 {
662     if (!isScopeInfoValid())
663         return;
664 
665     double const targetWFOV = getTargetWFOV();
666     double const targetHFOV = getTargetHFOV();
667 
668     if (ui->targetWFOVSpin->value() != targetWFOV)
669     {
670         bool const sig = ui->targetWFOVSpin->blockSignals(true);
671         ui->targetWFOVSpin->setValue(targetWFOV);
672         ui->targetWFOVSpin->blockSignals(sig);
673         updateTimer->start();
674     }
675 
676     if (ui->targetHFOVSpin->value() != targetHFOV)
677     {
678         bool const sig = ui->targetHFOVSpin->blockSignals(true);
679         ui->targetHFOVSpin->setValue(targetHFOV);
680         ui->targetHFOVSpin->blockSignals(sig);
681         updateTimer->start();
682     }
683 }
684 
updateGridFromTargetFOV()685 void Mosaic::updateGridFromTargetFOV()
686 {
687     if (!isScopeInfoValid())
688         return;
689 
690     double const expectedW = getTargetMosaicW();
691     double const expectedH = getTargetMosaicH();
692 
693     if (expectedW != ui->mosaicWSpin->value())
694     {
695         bool const sig = ui->mosaicWSpin->blockSignals(true);
696         ui->mosaicWSpin->setValue(expectedW);
697         ui->mosaicWSpin->blockSignals(sig);
698     }
699 
700     if (expectedH != ui->mosaicHSpin->value())
701     {
702         bool const sig = ui->mosaicHSpin->blockSignals(true);
703         ui->mosaicHSpin->setValue(expectedH);
704         ui->mosaicHSpin->blockSignals(sig);
705     }
706 
707     // Update unconditionally, as we may be updating the overlap or the target FOV covered by the mosaic
708     updateTimer->start();
709 }
710 
constructMosaic()711 void Mosaic::constructMosaic()
712 {
713     updateTimer->stop();
714 
715     if (!isScopeInfoValid())
716         return;
717 
718     updateTargetFOV();
719 
720     ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(ui->mosaicWSpin->value() > 1 || ui->mosaicHSpin->value() > 1);
721 
722     if (mosaicTileItem->getPA() != ui->rotationSpin->value())
723         Options::setCameraRotation(ui->rotationSpin->value());
724 
725     qCDebug(KSTARS_EKOS_SCHEDULER) << "Tile FOV in pixels W:" << ui->cameraWFOVSpin->value() * pixelsPerArcminRA << "H:"
726                                    << ui->cameraHFOVSpin->value() * pixelsPerArcminDE;
727 
728     mosaicTileItem->setSkyCenter(center);
729     mosaicTileItem->setGridDimensions(ui->mosaicWSpin->value(), ui->mosaicHSpin->value());
730     mosaicTileItem->setPositionAngle(ui->rotationSpin->value());
731     mosaicTileItem->setSingleTileFOV(ui->cameraWFOVSpin->value() * pixelsPerArcminRA, ui->cameraHFOVSpin->value() * pixelsPerArcminDE);
732     mosaicTileItem->setMosaicFOV(ui->targetWFOVSpin->value() * pixelsPerArcminRA, ui->targetHFOVSpin->value() * pixelsPerArcminDE);
733     mosaicTileItem->setOverlap(ui->overlapSpin->value() / 100);
734     mosaicTileItem->updateTiles(mosaicTileItem->mapToItem(skyMapItem, skyMapItem->boundingRect().center()), QSizeF(pixelsPerArcminRA*60.0, pixelsPerArcminDE*60.0), ui->reverseOddRows->checkState() == Qt::CheckState::Checked);
735 
736     ui->jobCountSpin->setValue(mosaicTileItem->getWidth() * mosaicTileItem->getHeight());
737 
738     if (ui->transparencyAuto->isChecked())
739     {
740         // Tiles should be more transparent when many are overlapped
741         // Overlap < 50%: low transparency, as only two tiles will overlap on a line
742         // 50% < Overlap < 75%: mid transparency, as three tiles will overlap one a line
743         // 75% < Overlap: high transparency, as four tiles will overlap on a line
744         // Slider controlling transparency provides [5%,50%], which is scaled to 0-200 alpha.
745 
746         if (1 < ui->jobCountSpin->value())
747             ui->transparencySlider->setValue(40 - ui->overlapSpin->value()/2);
748         else
749             ui->transparencySlider->setValue(40);
750 
751         ui->transparencySlider->update();
752     }
753 
754     resizeEvent(nullptr);
755     mosaicTileItem->show();
756 
757     ui->mosaicView->update();
758 }
759 
getJobs() const760 QList <Mosaic::Job> Mosaic::getJobs() const
761 {
762     qCDebug(KSTARS_EKOS_SCHEDULER) << "Mosaic Tile W:" << mosaicTileItem->boundingRect().width() << "H:" <<
763                                    mosaicTileItem->boundingRect().height();
764 
765     QList <Mosaic::Job> result;
766 
767     // We have two items:
768     // 1. SkyMapItem is the pixmap we fetch from KStars that shows the sky field.
769     // 2. MosaicItem is the constructed mosaic boxes.
770     // We already know the center (RA0,DE0) of the SkyMapItem.
771     // We Map the coordinate of each tile to the SkyMapItem to find out where the tile center is located
772     // on the SkyMapItem pixmap.
773     // We calculate the difference between the tile center and the SkyMapItem center and then find the tile coordinates
774     // in J2000 coords.
775     for (int i = 0; i < mosaicTileItem->getHeight(); i++)
776     {
777         for (int j = 0; j < mosaicTileItem->getWidth(); j++)
778         {
779             MosaicTile::OneTile * const tile = mosaicTileItem->getTile(i, j);
780             qCDebug(KSTARS_EKOS_SCHEDULER) << "Tile #" << i * mosaicTileItem->getWidth() + j << "Center:" << tile->center;
781 
782             Job ts;
783             ts.center.setRA0(tile->skyCenter.ra0().Hours());
784             ts.center.setDec0(tile->skyCenter.dec0().Degrees());
785             ts.rotation = -mosaicTileItem->getPA();
786 
787             ts.doAlign =
788                     (0 < ui->alignEvery->value()) &&
789                     (0 == ((j+i*mosaicTileItem->getHeight()) % ui->alignEvery->value()));
790 
791             ts.doFocus =
792                     (0 < ui->focusEvery->value()) &&
793                     (0 == ((j+i*mosaicTileItem->getHeight()) % ui->focusEvery->value()));
794 
795             qCDebug(KSTARS_EKOS_SCHEDULER) << "Tile RA0:" << tile->skyCenter.ra0().toHMSString() << "DE0:" <<
796                                            tile->skyCenter.dec0().toDMSString();
797             result.append(ts);
798         }
799     }
800 
801     return result;
802 }
803 
fetchINDIInformation()804 void Mosaic::fetchINDIInformation()
805 {
806     QDBusInterface alignInterface("org.kde.kstars",
807                                   "/KStars/Ekos/Align",
808                                   "org.kde.kstars.Ekos.Align",
809                                   QDBusConnection::sessionBus());
810 
811     QDBusReply<QList<double>> cameraReply = alignInterface.call("cameraInfo");
812     if (cameraReply.isValid())
813     {
814         QList<double> const values = cameraReply.value();
815 
816         setCameraSize(values[0], values[1]);
817         setPixelSize(values[2], values[3]);
818     }
819 
820     QDBusReply<QList<double>> telescopeReply = alignInterface.call("telescopeInfo");
821     if (telescopeReply.isValid())
822     {
823         QList<double> const values = telescopeReply.value();
824         setFocalLength(values[0]);
825     }
826 
827     QDBusReply<QList<double>> solutionReply = alignInterface.call("getSolutionResult");
828     if (solutionReply.isValid())
829     {
830         QList<double> const values = solutionReply.value();
831         if (values[0] > INVALID_VALUE)
832             ui->rotationSpin->setValue(values[0]);
833     }
834 
835     calculateFOV();
836 }
837 
rewordStepEvery(int v)838 void Mosaic::rewordStepEvery(int v)
839 {
840     QSpinBox * sp = dynamic_cast<QSpinBox *>(sender());
841     if (0 < v)
842         sp->setSuffix(i18np(" Scheduler job", " Scheduler jobs", v));
843     else
844         sp->setSuffix(i18n(" (first only)"));
845 }
846 
847 }
848