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(¢er);
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