1 /*
2 SPDX-FileCopyrightText: 2001 Jason Harris <jharris@30doradus.org>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 #include "skyobjectuserdata.h"
8 #ifdef _WIN32
9 #include <windows.h>
10 #endif
11
12 #include "skymap.h"
13
14 #include "ksasteroid.h"
15 #include "kstars_debug.h"
16 #include "fov.h"
17 #include "imageviewer.h"
18 #include "xplanetimageviewer.h"
19 #include "ksdssdownloader.h"
20 #include "kspaths.h"
21 #include "kspopupmenu.h"
22 #include "kstars.h"
23 #include "ksutils.h"
24 #include "Options.h"
25 #include "skymapcomposite.h"
26 #ifdef HAVE_OPENGL
27 #include "skymapgldraw.h"
28 #endif
29 #include "skymapqdraw.h"
30 #include "starhopperdialog.h"
31 #include "starobject.h"
32 #include "texturemanager.h"
33 #include "dialogs/detaildialog.h"
34 #include "printing/printingwizard.h"
35 #include "skycomponents/flagcomponent.h"
36 #include "skyobjects/ksplanetbase.h"
37 #include "skyobjects/satellite.h"
38 #include "tools/flagmanager.h"
39 #include "widgets/infoboxwidget.h"
40 #include "projections/azimuthalequidistantprojector.h"
41 #include "projections/equirectangularprojector.h"
42 #include "projections/lambertprojector.h"
43 #include "projections/gnomonicprojector.h"
44 #include "projections/orthographicprojector.h"
45 #include "projections/stereographicprojector.h"
46 #include "catalogobject.h"
47 #include "catalogsdb.h"
48 #include "catalogscomponent.h"
49
50 #include <KActionCollection>
51 #include <KToolBar>
52
53 #include <QBitmap>
54 #include <QToolTip>
55 #include <QClipboard>
56 #include <QInputDialog>
57 #include <QDesktopServices>
58
59 #include <QProcess>
60 #include <QFileDialog>
61
62 #include <cmath>
63
64 namespace
65 {
66 // Draw bitmap for zoom cursor. Width is size of pen to draw with.
zoomCursorBitmap(int width)67 QBitmap zoomCursorBitmap(int width)
68 {
69 QBitmap b(32, 32);
70 b.fill(Qt::color0);
71 int mx = 16, my = 16;
72 // Begin drawing
73 QPainter p;
74 p.begin(&b);
75 p.setPen(QPen(Qt::color1, width));
76 p.drawEllipse(mx - 7, my - 7, 14, 14);
77 p.drawLine(mx + 5, my + 5, mx + 11, my + 11);
78 p.end();
79 return b;
80 }
81
82 // Draw bitmap for default cursor. Width is size of pen to draw with.
defaultCursorBitmap(int width)83 QBitmap defaultCursorBitmap(int width)
84 {
85 QBitmap b(32, 32);
86 b.fill(Qt::color0);
87 int mx = 16, my = 16;
88 // Begin drawing
89 QPainter p;
90 p.begin(&b);
91 p.setPen(QPen(Qt::color1, width));
92 // 1. diagonal
93 p.drawLine(mx - 2, my - 2, mx - 8, mx - 8);
94 p.drawLine(mx + 2, my + 2, mx + 8, mx + 8);
95 // 2. diagonal
96 p.drawLine(mx - 2, my + 2, mx - 8, mx + 8);
97 p.drawLine(mx + 2, my - 2, mx + 8, mx - 8);
98 p.end();
99 return b;
100 }
101
circleCursorBitmap(int width)102 QBitmap circleCursorBitmap(int width)
103 {
104 QBitmap b(32, 32);
105 b.fill(Qt::color0);
106 int mx = 16, my = 16;
107 // Begin drawing
108 QPainter p;
109 p.begin(&b);
110 p.setPen(QPen(Qt::color1, width));
111
112 // Circle
113 p.drawEllipse(mx - 8, my - 8, mx, my);
114 // 1. diagonal
115 p.drawLine(mx - 8, my - 8, 0, 0);
116 p.drawLine(mx + 8, my - 8, 32, 0);
117 // 2. diagonal
118 p.drawLine(mx - 8, my + 8, 0, 32);
119 p.drawLine(mx + 8, my + 8, 32, 32);
120
121 p.end();
122 return b;
123 }
124
125 } // namespace
126
127 SkyMap *SkyMap::pinstance = nullptr;
128
Create()129 SkyMap *SkyMap::Create()
130 {
131 delete pinstance;
132 pinstance = new SkyMap();
133 return pinstance;
134 }
135
Instance()136 SkyMap *SkyMap::Instance()
137 {
138 return pinstance;
139 }
140
SkyMap()141 SkyMap::SkyMap()
142 : QGraphicsView(KStars::Instance()), computeSkymap(true), rulerMode(false), data(KStarsData::Instance()), pmenu(nullptr),
143 ClickedObject(nullptr), FocusObject(nullptr), m_proj(nullptr), m_previewLegend(false), m_objPointingMode(false)
144 {
145 #if !defined(KSTARS_LITE)
146 grabGesture(Qt::PinchGesture);
147 grabGesture(Qt::TapAndHoldGesture);
148 #endif
149 m_Scale = 1.0;
150
151 ZoomRect = QRect();
152
153 // set the default cursor
154 setMouseCursorShape(static_cast<Cursor>(Options::defaultCursor()));
155
156 QPalette p = palette();
157 p.setColor(QPalette::Window, QColor(data->colorScheme()->colorNamed("SkyColor")));
158 setPalette(p);
159
160 setFocusPolicy(Qt::StrongFocus);
161 setMinimumSize(380, 250);
162 setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding));
163 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
164 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
165 setStyleSheet("QGraphicsView { border-style: none; }");
166
167 setMouseTracking(true); //Generate MouseMove events!
168 midMouseButtonDown = false;
169 mouseButtonDown = false;
170 slewing = false;
171 clockSlewing = false;
172
173 ClickedObject = nullptr;
174 FocusObject = nullptr;
175
176 m_SkyMapDraw = nullptr;
177
178 pmenu = new KSPopupMenu();
179
180 setupProjector();
181
182 //Initialize Transient label stuff
183 m_HoverTimer.setSingleShot(true); // using this timer as a single shot timer
184
185 connect(&m_HoverTimer, SIGNAL(timeout()), this, SLOT(slotTransientLabel()));
186 connect(this, SIGNAL(destinationChanged()), this, SLOT(slewFocus()));
187 connect(KStarsData::Instance(), SIGNAL(skyUpdate(bool)), this, SLOT(slotUpdateSky(bool)));
188
189 // Time infobox
190 m_timeBox = new InfoBoxWidget(Options::shadeTimeBox(), Options::positionTimeBox(), Options::stickyTimeBox(),
191 QStringList(), this);
192 m_timeBox->setVisible(Options::showTimeBox());
193 connect(data->clock(), SIGNAL(timeChanged()), m_timeBox, SLOT(slotTimeChanged()));
194 connect(data->clock(), SIGNAL(timeAdvanced()), m_timeBox, SLOT(slotTimeChanged()));
195
196 // Geo infobox
197 m_geoBox = new InfoBoxWidget(Options::shadeGeoBox(), Options::positionGeoBox(), Options::stickyGeoBox(),
198 QStringList(), this);
199 m_geoBox->setVisible(Options::showGeoBox());
200 connect(data, SIGNAL(geoChanged()), m_geoBox, SLOT(slotGeoChanged()));
201
202 // Object infobox
203 m_objBox = new InfoBoxWidget(Options::shadeFocusBox(), Options::positionFocusBox(), Options::stickyFocusBox(),
204 QStringList(), this);
205 m_objBox->setVisible(Options::showFocusBox());
206 connect(this, SIGNAL(objectChanged(SkyObject*)), m_objBox, SLOT(slotObjectChanged(SkyObject*)));
207 connect(this, SIGNAL(positionChanged(SkyPoint*)), m_objBox, SLOT(slotPointChanged(SkyPoint*)));
208
209 m_SkyMapDraw = new SkyMapQDraw(this);
210 m_SkyMapDraw->setMouseTracking(true);
211
212 m_SkyMapDraw->setParent(this->viewport());
213 m_SkyMapDraw->show();
214
215 m_iboxes = new InfoBoxes(m_SkyMapDraw);
216
217 m_iboxes->setVisible(Options::showInfoBoxes());
218 m_iboxes->addInfoBox(m_timeBox);
219 m_iboxes->addInfoBox(m_geoBox);
220 m_iboxes->addInfoBox(m_objBox);
221 }
222
slotToggleGeoBox(bool flag)223 void SkyMap::slotToggleGeoBox(bool flag)
224 {
225 m_geoBox->setVisible(flag);
226 }
227
slotToggleFocusBox(bool flag)228 void SkyMap::slotToggleFocusBox(bool flag)
229 {
230 m_objBox->setVisible(flag);
231 }
232
slotToggleTimeBox(bool flag)233 void SkyMap::slotToggleTimeBox(bool flag)
234 {
235 m_timeBox->setVisible(flag);
236 }
237
slotToggleInfoboxes(bool flag)238 void SkyMap::slotToggleInfoboxes(bool flag)
239 {
240 m_iboxes->setVisible(flag);
241 Options::setShowInfoBoxes(flag);
242 }
243
~SkyMap()244 SkyMap::~SkyMap()
245 {
246 /* == Save infoxes status into Options == */
247 //Options::setShowInfoBoxes(m_iboxes->isVisibleTo(parentWidget()));
248 // Time box
249 Options::setPositionTimeBox(m_timeBox->pos());
250 Options::setShadeTimeBox(m_timeBox->shaded());
251 Options::setStickyTimeBox(m_timeBox->sticky());
252 Options::setShowTimeBox(m_timeBox->isVisibleTo(m_iboxes));
253 // Geo box
254 Options::setPositionGeoBox(m_geoBox->pos());
255 Options::setShadeGeoBox(m_geoBox->shaded());
256 Options::setStickyGeoBox(m_geoBox->sticky());
257 Options::setShowGeoBox(m_geoBox->isVisibleTo(m_iboxes));
258 // Obj box
259 Options::setPositionFocusBox(m_objBox->pos());
260 Options::setShadeFocusBox(m_objBox->shaded());
261 Options::setStickyFocusBox(m_objBox->sticky());
262 Options::setShowFocusBox(m_objBox->isVisibleTo(m_iboxes));
263
264 //store focus values in Options
265 //If not tracking and using Alt/Az coords, stor the Alt/Az coordinates
266 if (Options::useAltAz() && !Options::isTracking())
267 {
268 Options::setFocusRA(focus()->az().Degrees());
269 Options::setFocusDec(focus()->alt().Degrees());
270 }
271 else
272 {
273 Options::setFocusRA(focus()->ra().Hours());
274 Options::setFocusDec(focus()->dec().Degrees());
275 }
276
277 #ifdef HAVE_OPENGL
278 delete m_SkyMapGLDraw;
279 delete m_SkyMapQDraw;
280 m_SkyMapDraw = 0; // Just a formality
281 #else
282 delete m_SkyMapDraw;
283 #endif
284
285 delete pmenu;
286
287 delete m_proj;
288
289 pinstance = nullptr;
290 }
291
showFocusCoords()292 void SkyMap::showFocusCoords()
293 {
294 if (focusObject() && Options::isTracking())
295 emit objectChanged(focusObject());
296 else
297 emit positionChanged(focus());
298 }
299
slotTransientLabel()300 void SkyMap::slotTransientLabel()
301 {
302 //This function is only called if the HoverTimer manages to timeout.
303 //(HoverTimer is restarted with every mouseMoveEvent; so if it times
304 //out, that means there was no mouse movement for HOVER_INTERVAL msec.)
305 if (hasFocus() && !slewing &&
306 !(Options::useAltAz() && Options::showGround() && m_MousePoint.altRefracted().Degrees() < 0.0))
307 {
308 double maxrad = 1000.0 / Options::zoomFactor();
309 SkyObject *so = data->skyComposite()->objectNearest(&m_MousePoint, maxrad);
310
311 if (so && !isObjectLabeled(so))
312 {
313 QString name = so->translatedLongName();
314 if (!std::isnan(so->mag()))
315 name += QString(": %1<sup>m</sup>").arg(QString::number(so->mag(), 'f', 1));
316 QToolTip::showText(QCursor::pos(), name, this);
317 }
318 }
319 }
320
321 //Slots
322
setClickedObject(SkyObject * o)323 void SkyMap::setClickedObject(SkyObject *o)
324 {
325 ClickedObject = o;
326 }
327
setFocusObject(SkyObject * o)328 void SkyMap::setFocusObject(SkyObject *o)
329 {
330 FocusObject = o;
331 if (FocusObject)
332 Options::setFocusObject(FocusObject->name());
333 else
334 Options::setFocusObject(i18n("nothing"));
335 }
336
slotCenter()337 void SkyMap::slotCenter()
338 {
339 KStars *kstars = KStars::Instance();
340 TrailObject *trailObj = dynamic_cast<TrailObject *>(focusObject());
341
342 SkyPoint *foc;
343 if(ClickedObject != nullptr)
344 foc = ClickedObject;
345 else
346 foc = &ClickedPoint;
347
348 if (Options::useAltAz())
349 {
350 // JM 2016-09-12: Following call has problems when ra0/dec0 of an object are not valid for example
351 // because they're solar system bodies. So it creates a lot of issues. It is disabled and centering
352 // works correctly for all different body types as I tested.
353 //DeepSkyObject *dso = dynamic_cast<DeepSkyObject *>(focusObject());
354 //if (dso)
355 // foc->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false);
356
357 // JM 2018-05-06: No need to do the above
358 foc->EquatorialToHorizontal(data->lst(), data->geo()->lat());
359 }
360 else
361 foc->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false);
362
363 qCDebug(KSTARS) << "Centering on " << foc->ra().toHMSString() << foc->dec().toDMSString();
364
365 //clear the planet trail of old focusObject, if it was temporary
366 if (trailObj && data->temporaryTrail)
367 {
368 trailObj->clearTrail();
369 data->temporaryTrail = false;
370 }
371
372 //If the requested object is below the opaque horizon, issue a warning message
373 //(unless user is already pointed below the horizon)
374 if (Options::useAltAz() && Options::showGround() &&
375 focus()->alt().Degrees() > SkyPoint::altCrit &&
376 foc->alt().Degrees() <= SkyPoint::altCrit)
377 {
378 QString caption = i18n("Requested Position Below Horizon");
379 QString message = i18n("The requested position is below the horizon.\nWould you like to go there anyway?");
380 if (KMessageBox::warningYesNo(this, message, caption, KGuiItem(i18n("Go Anyway")),
381 KGuiItem(i18n("Keep Position")), "dag_focus_below_horiz") == KMessageBox::No)
382 {
383 setClickedObject(nullptr);
384 setFocusObject(nullptr);
385 Options::setIsTracking(false);
386
387 return;
388 }
389 }
390
391 //set FocusObject before slewing. Otherwise, KStarsData::updateTime() can reset
392 //destination to previous object...
393 setFocusObject(ClickedObject);
394 if(ClickedObject == nullptr)
395 setFocusPoint(&ClickedPoint);
396
397 Options::setIsTracking(true);
398
399 if (kstars)
400 {
401 kstars->actionCollection()
402 ->action("track_object")
403 ->setIcon(QIcon::fromTheme("document-encrypt"));
404 kstars->actionCollection()->action("track_object")->setText(i18n("Stop &Tracking"));
405 }
406
407 //If focusObject is a SS body and doesn't already have a trail, set the temporaryTrail
408
409 if (Options::useAutoTrail() && trailObj && trailObj->hasTrail())
410 {
411 trailObj->addToTrail();
412 data->temporaryTrail = true;
413 }
414
415 //update the destination to the selected coordinates
416 if (Options::useAltAz())
417 {
418 setDestinationAltAz(foc->alt(), foc->az(), false);
419 }
420 else
421 {
422 setDestination(*foc);
423 }
424
425 foc->EquatorialToHorizontal(data->lst(), data->geo()->lat());
426
427 //display coordinates in statusBar
428 emit mousePointChanged(foc);
429 showFocusCoords(); //update FocusBox
430 }
431
slotUpdateSky(bool now)432 void SkyMap::slotUpdateSky(bool now)
433 {
434 // Code moved from KStarsData::updateTime()
435 //Update focus
436 updateFocus();
437
438 if (now)
439 QTimer::singleShot(
440 0, this,
441 SLOT(forceUpdateNow())); // Why is it done this way rather than just calling forceUpdateNow()? -- asimha // --> Opening a neww thread? -- Valentin
442 else
443 forceUpdate();
444 }
445
slotDSS()446 void SkyMap::slotDSS()
447 {
448 dms ra(0.0), dec(0.0);
449 QString urlstring;
450
451 //ra and dec must be the coordinates at J2000. If we clicked on an object, just use the object's ra0, dec0 coords
452 //if we clicked on empty sky, we need to precess to J2000.
453 if (clickedObject())
454 {
455 urlstring = KSDssDownloader::getDSSURL(clickedObject());
456 }
457 else
458 {
459 //SkyPoint deprecessedPoint = clickedPoint()->deprecess(data->updateNum());
460 SkyPoint deprecessedPoint = clickedPoint()->catalogueCoord(data->updateNum()->julianDay());
461 ra = deprecessedPoint.ra();
462 dec = deprecessedPoint.dec();
463 urlstring = KSDssDownloader::getDSSURL(ra, dec); // Use default size for non-objects
464 }
465
466 QUrl url(urlstring);
467
468 KStars *kstars = KStars::Instance();
469 if (kstars)
470 {
471 new ImageViewer(
472 url, i18n("Digitized Sky Survey image provided by the Space Telescope Science Institute [free for non-commercial use]."),
473 this);
474 //iv->show();
475 }
476 }
477
slotCopyCoordinates()478 void SkyMap::slotCopyCoordinates()
479 {
480 dms J2000RA(0.0), J2000DE(0.0), JNowRA(0.0), JNowDE(0.0), Az, Alt;
481 if (clickedObject())
482 {
483 J2000RA = clickedObject()->ra0();
484 J2000DE = clickedObject()->dec0();
485 JNowRA = clickedObject()->ra();
486 JNowDE = clickedObject()->dec();
487 Az = clickedObject()->az();
488 Alt = clickedObject()->alt();
489 }
490 else
491 {
492 // Empty point only have valid JNow RA/DE, not J2000 information.
493 SkyPoint emptyPoint = *clickedPoint();
494 // Now get J2000 from JNow but de-aberrating, de-nutating, de-preccessing
495 // This modifies emptyPoint, but the RA/DE are now missing and need
496 // to be repopulated.
497 emptyPoint.catalogueCoord(data->updateNum()->julianDay());
498 emptyPoint.setRA(clickedPoint()->ra());
499 emptyPoint.setDec(clickedPoint()->dec());
500 emptyPoint.EquatorialToHorizontal(data->lst(), data->geo()->lat());
501
502 J2000RA = emptyPoint.ra0();
503 J2000DE = emptyPoint.dec0();
504 JNowRA = emptyPoint.ra();
505 JNowDE = emptyPoint.dec();
506 Az = emptyPoint.az();
507 Alt = emptyPoint.alt();
508 }
509
510 QApplication::clipboard()->setText(i18nc("Equatorial & Horizontal Coordinates",
511 "JNow:\t%1\t%2\nJ2000:\t%3\t%4\nAzAlt:\t%5\t%6",
512 JNowRA.toHMSString(),
513 JNowDE.toDMSString(),
514 J2000RA.toHMSString(),
515 J2000DE.toDMSString(),
516 Az.toDMSString(),
517 Alt.toDMSString()));
518 }
519
520
slotCopyTLE()521 void SkyMap::slotCopyTLE()
522 {
523
524 QString tle = "";
525 if (clickedObject()->type() == SkyObject::SATELLITE)
526 {
527 Satellite *sat = (Satellite *) clickedObject();
528 tle = sat->tle();
529 }
530 else
531 {
532 tle = "NO TLE FOR OBJECT";
533 }
534
535
536 QApplication::clipboard()->setText(tle);
537 }
538
slotSDSS()539 void SkyMap::slotSDSS()
540 {
541 // TODO: Remove code duplication -- we have the same stuff
542 // implemented in ObservingList::setCurrentImage() etc. in
543 // tools/observinglist.cpp; must try to de-duplicate as much as
544 // possible.
545 QString URLprefix("http://skyserver.sdss.org/dr16/SkyServerWS/ImgCutout/getjpeg?");
546 QString URLsuffix("&scale=1.0&width=600&height=600");
547 dms ra(0.0), dec(0.0);
548 QString RAString, DecString;
549
550 //ra and dec must be the coordinates at J2000. If we clicked on an object, just use the object's ra0, dec0 coords
551 //if we clicked on empty sky, we need to precess to J2000.
552 if (clickedObject())
553 {
554 ra = clickedObject()->ra0();
555 dec = clickedObject()->dec0();
556 }
557 else
558 {
559 //SkyPoint deprecessedPoint = clickedPoint()->deprecess(data->updateNum());
560 SkyPoint deprecessedPoint = clickedPoint()->catalogueCoord(data->updateNum()->julianDay());
561 deprecessedPoint.catalogueCoord(data->updateNum()->julianDay());
562 ra = deprecessedPoint.ra();
563 dec = deprecessedPoint.dec();
564 }
565
566 RAString = QString::asprintf("ra=%f", ra.Degrees());
567 DecString = QString::asprintf("&dec=%f", dec.Degrees());
568
569 //concat all the segments into the kview command line:
570 QUrl url(URLprefix + RAString + DecString + URLsuffix);
571
572 KStars *kstars = KStars::Instance();
573 if (kstars)
574 {
575 new ImageViewer(url,
576 i18n("Sloan Digital Sky Survey image provided by the Astrophysical Research Consortium [free "
577 "for non-commercial use]."),
578 this);
579 //iv->show();
580 }
581 }
582
slotEyepieceView()583 void SkyMap::slotEyepieceView()
584 {
585 KStars::Instance()->slotEyepieceView((clickedObject() ? clickedObject() : clickedPoint()));
586 }
slotBeginAngularDistance()587 void SkyMap::slotBeginAngularDistance()
588 {
589 beginRulerMode(false);
590 }
591
slotBeginStarHop()592 void SkyMap::slotBeginStarHop()
593 {
594 beginRulerMode(true);
595 }
596
beginRulerMode(bool starHopRuler)597 void SkyMap::beginRulerMode(bool starHopRuler)
598 {
599 rulerMode = true;
600 starHopDefineMode = starHopRuler;
601 AngularRuler.clear();
602
603 //If the cursor is near a SkyObject, reset the AngularRuler's
604 //start point to the position of the SkyObject
605 double maxrad = 1000.0 / Options::zoomFactor();
606 SkyObject *so = data->skyComposite()->objectNearest(clickedPoint(), maxrad);
607 if (so)
608 {
609 AngularRuler.append(so);
610 AngularRuler.append(so);
611 m_rulerStartPoint = so;
612 }
613 else
614 {
615 AngularRuler.append(clickedPoint());
616 AngularRuler.append(clickedPoint());
617 m_rulerStartPoint = clickedPoint();
618 }
619
620 AngularRuler.update(data);
621 }
622
slotEndRulerMode()623 void SkyMap::slotEndRulerMode()
624 {
625 if (!rulerMode)
626 return;
627 if (!starHopDefineMode) // Angular Ruler
628 {
629 QString sbMessage;
630
631 //If the cursor is near a SkyObject, reset the AngularRuler's
632 //end point to the position of the SkyObject
633 double maxrad = 1000.0 / Options::zoomFactor();
634 SkyPoint *rulerEndPoint;
635 SkyObject *so = data->skyComposite()->objectNearest(clickedPoint(), maxrad);
636 if (so)
637 {
638 AngularRuler.setPoint(1, so);
639 sbMessage = so->translatedLongName() + " ";
640 rulerEndPoint = so;
641 }
642 else
643 {
644 AngularRuler.setPoint(1, clickedPoint());
645 rulerEndPoint = clickedPoint();
646 }
647
648 rulerMode = false;
649 AngularRuler.update(data);
650 dms angularDistance = AngularRuler.angularSize();
651
652 sbMessage += i18n("Angular distance: %1", angularDistance.toDMSString());
653
654 const StarObject *p1 = dynamic_cast<const StarObject *>(m_rulerStartPoint);
655 const StarObject *p2 = dynamic_cast<const StarObject *>(rulerEndPoint);
656
657 qCDebug(KSTARS) << "Starobjects? " << p1 << p2;
658 if (p1 && p2)
659 qCDebug(KSTARS) << "Distances: " << p1->distance() << "pc; " << p2->distance() << "pc";
660 if (p1 && p2 && std::isfinite(p1->distance()) && std::isfinite(p2->distance()) && p1->distance() > 0 &&
661 p2->distance() > 0)
662 {
663 double dist = sqrt(p1->distance() * p1->distance() + p2->distance() * p2->distance() -
664 2 * p1->distance() * p2->distance() * cos(angularDistance.radians()));
665 qCDebug(KSTARS) << "Could calculate physical distance: " << dist << " pc";
666 sbMessage += i18n("; Physical distance: %1 pc", QString::number(dist));
667 }
668
669 AngularRuler.clear();
670
671 // Create unobsructive message box with suicidal tendencies
672 // to display result.
673 InfoBoxWidget *box = new InfoBoxWidget(true, mapFromGlobal(QCursor::pos()), 0, QStringList(sbMessage), this);
674 connect(box, SIGNAL(clicked()), box, SLOT(deleteLater()));
675 QTimer::singleShot(5000, box, SLOT(deleteLater()));
676 box->adjust();
677 box->show();
678 }
679 else // Star Hop
680 {
681 StarHopperDialog *shd = new StarHopperDialog(this);
682 const SkyPoint &startHop = *AngularRuler.point(0);
683 const SkyPoint &stopHop = *clickedPoint();
684 double fov; // Field of view in arcminutes
685 bool ok; // true if user did not cancel the operation
686 if (data->getAvailableFOVs().size() == 1)
687 {
688 // Exactly 1 FOV symbol visible, so use that. Also assume a circular FOV of size min{sizeX, sizeY}
689 FOV *f = data->getAvailableFOVs().first();
690 fov = ((f->sizeX() >= f->sizeY() && f->sizeY() != 0) ? f->sizeY() : f->sizeX());
691 ok = true;
692 }
693 else if (!data->getAvailableFOVs().isEmpty())
694 {
695 // Ask the user to choose from a list of available FOVs.
696 FOV const *f;
697 QMap<QString, double> nameToFovMap;
698 foreach (f, data->getAvailableFOVs())
699 {
700 nameToFovMap.insert(f->name(),
701 ((f->sizeX() >= f->sizeY() && f->sizeY() != 0) ? f->sizeY() : f->sizeX()));
702 }
703 fov = nameToFovMap[QInputDialog::getItem(this, i18n("Star Hopper: Choose a field-of-view"),
704 i18n("FOV to use for star hopping:"), nameToFovMap.uniqueKeys(), 0,
705 false, &ok)];
706 }
707 else
708 {
709 // Ask the user to enter a field of view
710 fov =
711 QInputDialog::getDouble(this, i18n("Star Hopper: Enter field-of-view to use"),
712 i18n("FOV to use for star hopping (in arcminutes):"), 60.0, 1.0, 600.0, 1, &ok);
713 }
714
715 Q_ASSERT(fov > 0.0);
716
717 if (ok)
718 {
719 qCDebug(KSTARS) << "fov = " << fov;
720
721 shd->starHop(startHop, stopHop, fov / 60.0, 9.0); //FIXME: Hardcoded maglimit value
722 shd->show();
723 }
724
725 rulerMode = false;
726 }
727 }
728
slotCancelRulerMode(void)729 void SkyMap::slotCancelRulerMode(void)
730 {
731 rulerMode = false;
732 AngularRuler.clear();
733 }
734
slotAddFlag()735 void SkyMap::slotAddFlag()
736 {
737 KStars *ks = KStars::Instance();
738
739 // popup FlagManager window and update coordinates
740 ks->slotFlagManager();
741 ks->flagManager()->clearFields();
742
743 //ra and dec must be the coordinates at J2000. If we clicked on an object, just use the object's ra0, dec0 coords
744 //if we clicked on empty sky, we need to precess to J2000.
745
746 dms J2000RA, J2000DE;
747
748 if (clickedObject())
749 {
750 J2000RA = clickedObject()->ra0();
751 J2000DE = clickedObject()->dec0();
752 }
753 else
754 {
755 //SkyPoint deprecessedPoint = clickedPoint()->deprecess(data->updateNum());
756 SkyPoint deprecessedPoint = clickedPoint()->catalogueCoord(data->updateNum()->julianDay());
757 deprecessedPoint.catalogueCoord(data->updateNum()->julianDay());
758 J2000RA = deprecessedPoint.ra();
759 J2000DE = deprecessedPoint.dec();
760 }
761
762 ks->flagManager()->setRaDec(J2000RA, J2000DE);
763 }
764
slotEditFlag(int flagIdx)765 void SkyMap::slotEditFlag(int flagIdx)
766 {
767 KStars *ks = KStars::Instance();
768
769 // popup FlagManager window and switch to selected flag
770 ks->slotFlagManager();
771 ks->flagManager()->showFlag(flagIdx);
772 }
773
slotDeleteFlag(int flagIdx)774 void SkyMap::slotDeleteFlag(int flagIdx)
775 {
776 KStars *ks = KStars::Instance();
777
778 ks->data()->skyComposite()->flags()->remove(flagIdx);
779 ks->data()->skyComposite()->flags()->saveToFile();
780
781 // if there is FlagManager created, update its flag model
782 if (ks->flagManager())
783 {
784 ks->flagManager()->deleteFlagItem(flagIdx);
785 }
786 }
787
slotImage()788 void SkyMap::slotImage()
789 {
790 const auto *action = qobject_cast<QAction *>(sender());
791 const auto url = action->data().toUrl();
792 const QString message{ action->text().remove('&') };
793
794 if (!url.isEmpty())
795 new ImageViewer(url, clickedObject()->messageFromTitle(message), this);
796 }
797
slotInfo()798 void SkyMap::slotInfo()
799 {
800 const auto *action = qobject_cast<QAction *>(sender());
801 const auto url = action->data().toUrl();
802
803 if (!url.isEmpty())
804 QDesktopServices::openUrl(url);
805 }
806
isObjectLabeled(SkyObject * object)807 bool SkyMap::isObjectLabeled(SkyObject *object)
808 {
809 return data->skyComposite()->labelObjects().contains(object);
810 }
811
getCenterPoint()812 SkyPoint SkyMap::getCenterPoint()
813 {
814 SkyPoint retVal;
815 // FIXME: subtraction of these 0.00001 is a simple workaround, because wrong
816 // SkyPoint is returned when _exact_ center of SkyMap is passed to the projector.
817 retVal = projector()->fromScreen(QPointF((qreal)width() / 2 - 0.00001, (qreal)height() / 2 - 0.00001), data->lst(),
818 data->geo()->lat());
819 return retVal;
820 }
821
slotRemoveObjectLabel()822 void SkyMap::slotRemoveObjectLabel()
823 {
824 data->skyComposite()->removeNameLabel(clickedObject());
825 forceUpdate();
826 }
827
slotRemoveCustomObject()828 void SkyMap::slotRemoveCustomObject()
829 {
830 auto *object = dynamic_cast<CatalogObject *>(clickedObject());
831 if (!object)
832 return;
833
834 const auto &cat = object->getCatalog();
835 if (!cat.mut)
836 return;
837
838 CatalogsDB::DBManager manager{ CatalogsDB::dso_db_path() };
839 manager.remove_object(cat.id, object->getObjectId());
840
841 emit removeSkyObject(object);
842 data->skyComposite()->removeFromNames(object);
843 data->skyComposite()->removeFromLists(object);
844 data->skyComposite()->reloadDeepSky();
845 KStars::Instance()->updateTime();
846 }
847
slotAddObjectLabel()848 void SkyMap::slotAddObjectLabel()
849 {
850 data->skyComposite()->addNameLabel(clickedObject());
851 forceUpdate();
852 }
853
slotRemovePlanetTrail()854 void SkyMap::slotRemovePlanetTrail()
855 {
856 TrailObject *tobj = dynamic_cast<TrailObject *>(clickedObject());
857 if (tobj)
858 {
859 tobj->clearTrail();
860 forceUpdate();
861 }
862 }
863
slotAddPlanetTrail()864 void SkyMap::slotAddPlanetTrail()
865 {
866 TrailObject *tobj = dynamic_cast<TrailObject *>(clickedObject());
867 if (tobj)
868 {
869 tobj->addToTrail();
870 forceUpdate();
871 }
872 }
873
slotDetail()874 void SkyMap::slotDetail()
875 {
876 // check if object is selected
877 if (!clickedObject())
878 {
879 KMessageBox::sorry(this, i18n("No object selected."), i18n("Object Details"));
880 return;
881 }
882 DetailDialog *detail = new DetailDialog(clickedObject(), data->ut(), data->geo(), KStars::Instance());
883 detail->setAttribute(Qt::WA_DeleteOnClose);
884 detail->show();
885 }
886
slotObjectSelected()887 void SkyMap::slotObjectSelected()
888 {
889 if (m_objPointingMode && KStars::Instance()->printingWizard())
890 {
891 KStars::Instance()->printingWizard()->pointingDone(clickedObject());
892 m_objPointingMode = false;
893 }
894 }
895
slotCancelLegendPreviewMode()896 void SkyMap::slotCancelLegendPreviewMode()
897 {
898 m_previewLegend = false;
899 forceUpdate(true);
900 KStars::Instance()->showImgExportDialog();
901 }
902
slotFinishFovCaptureMode()903 void SkyMap::slotFinishFovCaptureMode()
904 {
905 if (m_fovCaptureMode && KStars::Instance()->printingWizard())
906 {
907 KStars::Instance()->printingWizard()->fovCaptureDone();
908 m_fovCaptureMode = false;
909 }
910 }
911
slotCaptureFov()912 void SkyMap::slotCaptureFov()
913 {
914 if (KStars::Instance()->printingWizard())
915 {
916 KStars::Instance()->printingWizard()->captureFov();
917 }
918 }
919
slotClockSlewing()920 void SkyMap::slotClockSlewing()
921 {
922 //If the current timescale exceeds slewTimeScale, set clockSlewing=true, and stop the clock.
923 if ((fabs(data->clock()->scale()) > Options::slewTimeScale()) ^ clockSlewing)
924 {
925 data->clock()->setManualMode(!clockSlewing);
926 clockSlewing = !clockSlewing;
927 // don't change automatically the DST status
928 KStars *kstars = KStars::Instance();
929 if (kstars)
930 kstars->updateTime(false);
931 }
932 }
933
setFocus(SkyPoint * p)934 void SkyMap::setFocus(SkyPoint *p)
935 {
936 setFocus(p->ra(), p->dec());
937 }
938
setFocus(const dms & ra,const dms & dec)939 void SkyMap::setFocus(const dms &ra, const dms &dec)
940 {
941 Options::setFocusRA(ra.Hours());
942 Options::setFocusDec(dec.Degrees());
943
944 focus()->set(ra, dec);
945 focus()->EquatorialToHorizontal(data->lst(), data->geo()->lat());
946 }
947
setFocusAltAz(const dms & alt,const dms & az)948 void SkyMap::setFocusAltAz(const dms &alt, const dms &az)
949 {
950 Options::setFocusRA(focus()->ra().Hours());
951 Options::setFocusDec(focus()->dec().Degrees());
952 focus()->setAlt(alt);
953 focus()->setAz(az);
954 focus()->HorizontalToEquatorial(data->lst(), data->geo()->lat());
955
956 slewing = false;
957 forceUpdate(); //need a total update, or slewing with the arrow keys doesn't work.
958 }
959
setDestination(const SkyPoint & p)960 void SkyMap::setDestination(const SkyPoint &p)
961 {
962 setDestination(p.ra(), p.dec());
963 }
964
setDestination(const dms & ra,const dms & dec)965 void SkyMap::setDestination(const dms &ra, const dms &dec)
966 {
967 destination()->set(ra, dec);
968 destination()->EquatorialToHorizontal(data->lst(), data->geo()->lat());
969 emit destinationChanged();
970 }
971
setDestinationAltAz(const dms & alt,const dms & az,bool altIsRefracted)972 void SkyMap::setDestinationAltAz(const dms &alt, const dms &az, bool altIsRefracted)
973 {
974 if (altIsRefracted)
975 {
976 // The alt in the SkyPoint is always actual, not apparent
977 destination()->setAlt(SkyPoint::unrefract(alt));
978 }
979 else
980 {
981 destination()->setAlt(alt);
982 }
983 destination()->setAz(az);
984 destination()->HorizontalToEquatorial(data->lst(), data->geo()->lat());
985 emit destinationChanged();
986 }
987
setClickedPoint(const SkyPoint * f)988 void SkyMap::setClickedPoint(const SkyPoint *f)
989 {
990 ClickedPoint = *f;
991 }
992
updateFocus()993 void SkyMap::updateFocus()
994 {
995 if (slewing)
996 return;
997
998 //Tracking on an object
999 if (Options::isTracking() && focusObject() != nullptr)
1000 {
1001 if (Options::useAltAz())
1002 {
1003 //Tracking any object in Alt/Az mode requires focus updates
1004 focusObject()->EquatorialToHorizontal(data->lst(), data->geo()->lat());
1005 setFocusAltAz(focusObject()->alt(), focusObject()->az());
1006 focus()->HorizontalToEquatorial(data->lst(), data->geo()->lat());
1007 setDestination(*focus());
1008 }
1009 else
1010 {
1011 //Tracking in equatorial coords
1012 setFocus(focusObject());
1013 focus()->EquatorialToHorizontal(data->lst(), data->geo()->lat());
1014 setDestination(*focus());
1015 }
1016
1017 //Tracking on empty sky
1018 }
1019 else if (Options::isTracking() && focusPoint() != nullptr)
1020 {
1021 if (Options::useAltAz())
1022 {
1023 //Tracking on empty sky in Alt/Az mode
1024 setFocus(focusPoint());
1025 focus()->EquatorialToHorizontal(data->lst(), data->geo()->lat());
1026 setDestination(*focus());
1027 }
1028
1029 // Not tracking and not slewing, let sky drift by
1030 // This means that horizontal coordinates are constant.
1031 }
1032 else
1033 {
1034 focus()->HorizontalToEquatorial(data->lst(), data->geo()->lat());
1035 }
1036 }
1037
slewFocus()1038 void SkyMap::slewFocus()
1039 {
1040 //Don't slew if the mouse button is pressed
1041 //Also, no animated slews if the Manual Clock is active
1042 //08/2002: added possibility for one-time skipping of slew with snapNextFocus
1043 if (!mouseButtonDown)
1044 {
1045 bool goSlew = (Options::useAnimatedSlewing() && !data->snapNextFocus()) &&
1046 !(data->clock()->isManualMode() && data->clock()->isActive());
1047 if (goSlew)
1048 {
1049 double dX, dY;
1050 double maxstep = 10.0;
1051 if (Options::useAltAz())
1052 {
1053 dX = destination()->az().Degrees() - focus()->az().Degrees();
1054 dY = destination()->alt().Degrees() - focus()->alt().Degrees();
1055 }
1056 else
1057 {
1058 dX = destination()->ra().Degrees() - focus()->ra().Degrees();
1059 dY = destination()->dec().Degrees() - focus()->dec().Degrees();
1060 }
1061
1062 //switch directions to go the short way around the celestial sphere, if necessary.
1063 dX = KSUtils::reduceAngle(dX, -180.0, 180.0);
1064
1065 double r0 = sqrt(dX * dX + dY * dY);
1066 if (r0 < 20.0) //smaller slews have smaller maxstep
1067 {
1068 maxstep *= (10.0 + 0.5 * r0) / 20.0;
1069 }
1070 double step = 0.5;
1071 double r = r0;
1072 while (r > step)
1073 {
1074 //DEBUG
1075 //qDebug() << step << ": " << r << ": " << r0;
1076 double fX = dX / r;
1077 double fY = dY / r;
1078
1079 if (Options::useAltAz())
1080 {
1081 focus()->setAlt(focus()->alt().Degrees() + fY * step);
1082 focus()->setAz(dms(focus()->az().Degrees() + fX * step).reduce());
1083 focus()->HorizontalToEquatorial(data->lst(), data->geo()->lat());
1084 }
1085 else
1086 {
1087 fX = fX / 15.; //convert RA degrees to hours
1088 SkyPoint newFocus(focus()->ra().Hours() + fX * step, focus()->dec().Degrees() + fY * step);
1089 setFocus(&newFocus);
1090 focus()->EquatorialToHorizontal(data->lst(), data->geo()->lat());
1091 }
1092
1093 slewing = true;
1094
1095 forceUpdate();
1096 qApp->processEvents(); //keep up with other stuff
1097
1098 if (Options::useAltAz())
1099 {
1100 dX = destination()->az().Degrees() - focus()->az().Degrees();
1101 dY = destination()->alt().Degrees() - focus()->alt().Degrees();
1102 }
1103 else
1104 {
1105 dX = destination()->ra().Degrees() - focus()->ra().Degrees();
1106 dY = destination()->dec().Degrees() - focus()->dec().Degrees();
1107 }
1108
1109 //switch directions to go the short way around the celestial sphere, if necessary.
1110 dX = KSUtils::reduceAngle(dX, -180.0, 180.0);
1111 r = sqrt(dX * dX + dY * dY);
1112
1113 //Modify step according to a cosine-shaped profile
1114 //centered on the midpoint of the slew
1115 //NOTE: don't allow the full range from -PI/2 to PI/2
1116 //because the slew will never reach the destination as
1117 //the speed approaches zero at the end!
1118 double t = dms::PI * (r - 0.5 * r0) / (1.05 * r0);
1119 step = cos(t) * maxstep;
1120 }
1121 }
1122
1123 //Either useAnimatedSlewing==false, or we have slewed, and are within one step of destination
1124 //set focus=destination.
1125 if (Options::useAltAz())
1126 {
1127 setFocusAltAz(destination()->alt(), destination()->az());
1128 focus()->HorizontalToEquatorial(data->lst(), data->geo()->lat());
1129 }
1130 else
1131 {
1132 setFocus(destination());
1133 focus()->EquatorialToHorizontal(data->lst(), data->geo()->lat());
1134 }
1135
1136 slewing = false;
1137
1138 //Turn off snapNextFocus, we only want it to happen once
1139 if (data->snapNextFocus())
1140 {
1141 data->setSnapNextFocus(false);
1142 }
1143
1144 //Start the HoverTimer. if the user leaves the mouse in place after a slew,
1145 //we want to attach a label to the nearest object.
1146 if (Options::useHoverLabel())
1147 m_HoverTimer.start(HOVER_INTERVAL);
1148
1149 forceUpdate();
1150 }
1151 }
1152
slotZoomIn()1153 void SkyMap::slotZoomIn()
1154 {
1155 setZoomFactor(Options::zoomFactor() * DZOOM);
1156 }
1157
slotZoomOut()1158 void SkyMap::slotZoomOut()
1159 {
1160 setZoomFactor(Options::zoomFactor() / DZOOM);
1161 }
1162
slotZoomDefault()1163 void SkyMap::slotZoomDefault()
1164 {
1165 setZoomFactor(DEFAULTZOOM);
1166 }
1167
setZoomFactor(double factor)1168 void SkyMap::setZoomFactor(double factor)
1169 {
1170 Options::setZoomFactor(KSUtils::clamp(factor, MINZOOM, MAXZOOM));
1171 forceUpdate();
1172 emit zoomChanged();
1173 }
1174
1175 // force a new calculation of the skymap (used instead of update(), which may skip the redraw)
1176 // if now=true, SkyMap::paintEvent() is run immediately, rather than being added to the event queue
1177 // also, determine new coordinates of mouse cursor.
forceUpdate(bool now)1178 void SkyMap::forceUpdate(bool now)
1179 {
1180 QPoint mp(mapFromGlobal(QCursor::pos()));
1181 if (!projector()->unusablePoint(mp))
1182 {
1183 //determine RA, Dec of mouse pointer
1184 m_MousePoint = projector()->fromScreen(mp, data->lst(), data->geo()->lat());
1185 }
1186
1187 computeSkymap = true;
1188
1189 // Ensure that stars are recomputed
1190 data->incUpdateID();
1191
1192 if (now)
1193 m_SkyMapDraw->repaint();
1194 else
1195 m_SkyMapDraw->update();
1196 }
1197
fov()1198 float SkyMap::fov()
1199 {
1200 float diagonalPixels = sqrt(static_cast<double>(width() * width() + height() * height()));
1201 return diagonalPixels / (2 * Options::zoomFactor() * dms::DegToRad);
1202 }
1203
setupProjector()1204 void SkyMap::setupProjector()
1205 {
1206 //Update View Parameters for projection
1207 ViewParams p;
1208 p.focus = focus();
1209 p.height = height();
1210 p.width = width();
1211 p.useAltAz = Options::useAltAz();
1212 p.useRefraction = Options::useRefraction();
1213 p.zoomFactor = Options::zoomFactor();
1214 p.fillGround = Options::showGround();
1215 //Check if we need a new projector
1216 if (m_proj && Options::projection() == m_proj->type())
1217 m_proj->setViewParams(p);
1218 else
1219 {
1220 delete m_proj;
1221 switch (Options::projection())
1222 {
1223 case Gnomonic:
1224 m_proj = new GnomonicProjector(p);
1225 break;
1226 case Stereographic:
1227 m_proj = new StereographicProjector(p);
1228 break;
1229 case Orthographic:
1230 m_proj = new OrthographicProjector(p);
1231 break;
1232 case AzimuthalEquidistant:
1233 m_proj = new AzimuthalEquidistantProjector(p);
1234 break;
1235 case Equirectangular:
1236 m_proj = new EquirectangularProjector(p);
1237 break;
1238 case Lambert:
1239 default:
1240 //TODO: implement other projection classes
1241 m_proj = new LambertProjector(p);
1242 break;
1243 }
1244 }
1245 }
1246
setZoomMouseCursor()1247 void SkyMap::setZoomMouseCursor()
1248 {
1249 mouseMoveCursor = false; // no mousemove cursor
1250 QBitmap cursor = zoomCursorBitmap(2);
1251 QBitmap mask = zoomCursorBitmap(4);
1252 setCursor(QCursor(cursor, mask));
1253 }
1254
setMouseCursorShape(Cursor type)1255 void SkyMap::setMouseCursorShape(Cursor type)
1256 {
1257 // no mousemove cursor
1258 mouseMoveCursor = false;
1259
1260 switch (type)
1261 {
1262 case Cross:
1263 {
1264 QBitmap cursor = defaultCursorBitmap(2);
1265 QBitmap mask = defaultCursorBitmap(3);
1266 setCursor(QCursor(cursor, mask));
1267 }
1268 break;
1269
1270 case Circle:
1271 {
1272 QBitmap cursor = circleCursorBitmap(2);
1273 QBitmap mask = circleCursorBitmap(3);
1274 setCursor(QCursor(cursor, mask));
1275 }
1276 break;
1277
1278 case NoCursor:
1279 setCursor(Qt::ArrowCursor);
1280 break;
1281 }
1282 }
1283
setMouseMoveCursor()1284 void SkyMap::setMouseMoveCursor()
1285 {
1286 if (mouseButtonDown)
1287 {
1288 setCursor(Qt::SizeAllCursor); // cursor shape defined in qt
1289 mouseMoveCursor = true;
1290 }
1291 }
1292
updateAngleRuler()1293 void SkyMap::updateAngleRuler()
1294 {
1295 if (rulerMode && (!pmenu || !pmenu->isVisible()))
1296 AngularRuler.setPoint(1, &m_MousePoint);
1297 AngularRuler.update(data);
1298 }
1299
isSlewing() const1300 bool SkyMap::isSlewing() const
1301 {
1302 return (slewing || (clockSlewing && data->clock()->isActive()));
1303 }
1304
slotStartXplanetViewer()1305 void SkyMap::slotStartXplanetViewer()
1306 {
1307 if(clickedObject())
1308 new XPlanetImageViewer(clickedObject()->name(), this);
1309 else
1310 new XPlanetImageViewer(i18n("Saturn"), this);
1311 }
1312
1313
1314