1 /*  Artificial Horizon UI test
2     SPDX-FileCopyrightText: 2021 Hy Murveit <hy@murveit.com>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "test_artificial_horizon.h"
8 
9 #if defined(HAVE_INDI)
10 
11 #include <QStandardItemModel>
12 
13 #include "artificialhorizoncomponent.h"
14 #include "kstars_ui_tests.h"
15 #include "horizonmanager.h"
16 #include "linelist.h"
17 #include "skycomponents/skymapcomposite.h"
18 #include "skymap.h"
19 #include "test_ekos.h"
20 
TestArtificialHorizon(QObject * parent)21 TestArtificialHorizon::TestArtificialHorizon(QObject *parent) : QObject(parent)
22 {
23 }
24 
initTestCase()25 void TestArtificialHorizon::initTestCase()
26 {
27     // HACK: Reset clock to initial conditions
28     KHACK_RESET_EKOS_TIME();
29 }
30 
cleanupTestCase()31 void TestArtificialHorizon::cleanupTestCase()
32 {
33 }
34 
init()35 void TestArtificialHorizon::init()
36 {
37 
38 }
39 
cleanup()40 void TestArtificialHorizon::cleanup()
41 {
42 
43 }
44 
testArtificialHorizon_data()45 void TestArtificialHorizon::testArtificialHorizon_data()
46 {
47 
48 }
49 
50 namespace
51 {
52 
skyClick(SkyMap * sky,int x,int y)53 void skyClick(SkyMap *sky, int x, int y)
54 {
55     QTest::mouseClick(sky->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(x, y), 100);
56 }
57 
58 // Returns the QModelIndex for the nth point in the mth region
pointIndex(QStandardItemModel * model,int region,int point)59 QModelIndex pointIndex(QStandardItemModel *model, int region, int point)
60 {
61     QModelIndex badIndex;
62     if (model == nullptr)
63         return badIndex;
64     auto reg = model->item(region, 0);
65     if (reg == nullptr)
66         return badIndex;
67     auto child = reg->child(point, 1);
68     if (child == nullptr)
69         return badIndex;
70     return child->index();
71 }
72 
73 // Returns the QModelIndex for the nth region .
regionIndex(QStandardItemModel * model,int region)74 QModelIndex regionIndex(QStandardItemModel *model, int region)
75 {
76     QModelIndex badIndex;
77     if (model == nullptr)
78         return badIndex;
79     QModelIndex idx = model->index(region, 0);
80     return idx;
81 }
82 
83 // Simulates a left mouse clock on the given view.
84 // The click is centered vertically, and offset by leftOffset from the left size of the view.
clickView(QAbstractItemView * view,const QModelIndex & idx,int leftOffset)85 bool clickView(QAbstractItemView *view, const QModelIndex &idx, int leftOffset)
86 {
87     if (!idx.isValid())
88         return false;
89     QPoint itemPtCenter = view->visualRect(idx).center();
90     if (itemPtCenter.isNull())
91         return false;
92     QPoint itemPtLeft = itemPtCenter;
93     itemPtLeft.setX(view->visualRect(idx).left() + leftOffset);
94     QTest::mouseClick(view->viewport(), Qt::LeftButton, 0, itemPtLeft);
95     return true;
96 }
97 
98 // Simulates a click on the enable checkbox for the region view.
clickEnableRegion(QAbstractItemView * view,int region)99 bool clickEnableRegion(QAbstractItemView *view, int region)
100 {
101     QStandardItemModel *model = static_cast<QStandardItemModel*>(view->model());
102     const QModelIndex index = regionIndex(model, region);
103     if (!index.isValid()) return false;
104     // The checkbox is on the far left, so left offset is 5.
105     return clickView(view, index, 5);
106 }
107 
108 // Simulates a click on the nth region, in the region view.
clickSelectRegion(QAbstractItemView * view,int region)109 bool clickSelectRegion(QAbstractItemView *view, int region)
110 {
111     QStandardItemModel *model = static_cast<QStandardItemModel*>(view->model());
112     const QModelIndex index = regionIndex(model, region);
113     if (!index.isValid()) return false;
114     // Clicks near the middle of the box (left offset of 50).
115     return clickView(view, index, 50);
116 }
117 
118 // Simulates a click on the nth point, in the point list view.
clickSelectPoint(QAbstractItemView * view,int region,int point)119 bool clickSelectPoint(QAbstractItemView *view, int region, int point)
120 {
121     QStandardItemModel *model = static_cast<QStandardItemModel*>(view->model());
122     const QModelIndex index = pointIndex(model, region, point);
123     // Clicks near the middle of he box (left offset of 50).
124     return clickView(view, index, 50);
125 }
126 
127 #if 0
128 // Debugging printout. Prints the list of az/alt points in a region.
129 bool printAzAlt(QStandardItemModel *model, int region)
130 {
131     if (model->rowCount() <= region)
132         return false;
133 
134     const auto reg = model->item(region);
135     const int numPoints = reg->rowCount();
136     for (int i = 0; i < numPoints; ++i)
137     {
138         QStandardItem *azItem  = reg->child(i, 1);
139         QStandardItem *altItem = reg->child(i, 2);
140 
141         const dms az  = dms::fromString(azItem->data(Qt::DisplayRole).toString(), true);
142         const dms alt = dms::fromString(altItem->data(Qt::DisplayRole).toString(), true);
143 
144         fprintf(stderr, "az %f alt %f\n", az.Degrees(), alt.Degrees());
145     }
146     return true;
147 }
148 #endif
149 
150 }  // namespace
151 
152 // Returns the nth region.
getRegion(int region)153 QStandardItem *TestArtificialHorizon::getRegion(int region)
154 {
155     return m_Model->item(region);
156 }
157 
158 
159 // Creates a list of SkyPoints corresponding to the points in the nth region.
getRegionPoints(int region)160 QList<SkyPoint> TestArtificialHorizon::getRegionPoints(int region)
161 {
162     const auto reg = getRegion(region);
163     const int numPoints = reg->rowCount();
164     QList<SkyPoint> pts;
165     for (int i = 0; i < numPoints; ++i)
166     {
167         QStandardItem *azItem  = reg->child(i, 1);
168         QStandardItem *altItem = reg->child(i, 2);
169         const dms az  = dms::fromString(azItem->data(Qt::DisplayRole).toString(), true);
170         const dms alt = dms::fromString(altItem->data(Qt::DisplayRole).toString(), true);
171         SkyPoint p;
172         p.setAz(az);
173         p.setAlt(alt);
174         pts.append(p);
175     }
176     return pts;
177 }
178 
179 // Returns true if the SkyList contains the same az/alt points as the region.
compareLivePreview(int region,SkyList * previewPoints)180 bool TestArtificialHorizon::compareLivePreview(int region, SkyList *previewPoints)
181 {
182     if (m_Model->rowCount() <= region)
183         return false;
184     QList<SkyPoint> regionPoints = getRegionPoints(region);
185     if (previewPoints->size() != regionPoints.size())
186         return false;
187     for (int i = 0; i < regionPoints.size(); ++i)
188     {
189         if (previewPoints->at(i)->az().Degrees() != regionPoints[i].az().Degrees() ||
190                 previewPoints->at(i)->alt().Degrees() != regionPoints[i].alt().Degrees())
191             return false;
192     }
193     return true;
194 }
195 
196 // Checks for a testing bug where all az/alt points were repeated.
checkForRepeatedAzAlt(int region)197 bool TestArtificialHorizon::checkForRepeatedAzAlt(int region)
198 {
199     if (m_Model->rowCount() <= region)
200         return false;
201     const auto reg = getRegion(region);
202     const int numPoints = reg->rowCount();
203     double azKeep = 0, altKeep = 0;
204     for (int i = 0; i < numPoints; ++i)
205     {
206         QStandardItem *azItem  = reg->child(i, 1);
207         QStandardItem *altItem = reg->child(i, 2);
208 
209         const dms az  = dms::fromString(azItem->data(Qt::DisplayRole).toString(), true);
210         const dms alt = dms::fromString(altItem->data(Qt::DisplayRole).toString(), true);
211 
212         if (i == 0)
213         {
214             azKeep = az.Degrees();
215             altKeep = alt.Degrees();
216         }
217         else
218         {
219             if (az.Degrees() == azKeep || altKeep == alt.Degrees())
220             {
221                 fprintf(stderr, "Repeated point in Region %d pt %d: %f %f\n", region, i, az.Degrees(), alt.Degrees());
222                 return false;
223             }
224         }
225     }
226     return true;
227 }
228 
testArtificialHorizon()229 void TestArtificialHorizon::testArtificialHorizon()
230 {
231     // Open the Artificial Horizon menu and instantiate the interface.
232     KStars::Instance()->slotHorizonManager();
233     SkyMap *skymap = KStars::Instance()->map();
234 
235     ArtificialHorizonComponent *horizonComponent = KStarsData::Instance()->skyComposite()->artificialHorizon();
236 
237     // Region buttons
238     KTRY_AH_GADGET(QPushButton, addRegionB);
239     KTRY_AH_GADGET(QPushButton, removeRegionB);
240     KTRY_AH_GADGET(QPushButton, toggleCeilingB);
241     KTRY_AH_GADGET(QPushButton, saveB);
242     // Points buttons
243     KTRY_AH_GADGET(QPushButton, addPointB);
244     KTRY_AH_GADGET(QPushButton, removePointB);
245     KTRY_AH_GADGET(QPushButton, clearPointsB);
246     KTRY_AH_GADGET(QPushButton, selectPointsB);
247     // Views
248     KTRY_AH_GADGET(QTableView, pointsList);
249     KTRY_AH_GADGET(QListView, regionsList);
250 
251     // This is the underlying data structure for the entire UI.
252     m_Model = static_cast<QStandardItemModel*>(regionsList->model());
253 
254     // There shouldn't be any regions at the start.
255     QVERIFY(regionsList->model()->rowCount() == 0);
256 
257     // There should be a live preview.
258     QVERIFY(!horizonComponent->livePreview.get());
259 
260     // Add a region.
261     KTRY_AH_CLICK(addRegionB);
262 
263     // There should now be  one region, it has no points, is checked, and named "Region 1".
264     QVERIFY(regionsList->currentIndex().row() == 0);
265     QVERIFY(getRegion(0)->rowCount() == 0);
266     QVERIFY(getRegion(0)->checkState() == Qt::Checked);
267     QVERIFY(m_Model->index(0, 0).data( Qt::DisplayRole ).toString() == QString("Region 1"));
268 
269     // Check we can toggle on and off "enable" with a mouse click.
270     QVERIFY(clickEnableRegion(regionsList, 0));
271     QVERIFY(getRegion(0)->checkState() == Qt::Unchecked);
272     QVERIFY(clickEnableRegion(regionsList, 0));
273     QVERIFY(getRegion(0)->checkState() == Qt::Checked);
274 
275     // Mouse-click entry of points shouldn't be enabled yet.
276     QVERIFY(!selectPointsB->isChecked());
277 
278     // Enable mouse-click entry of points
279     KTRY_AH_CLICK(selectPointsB);
280     QVERIFY(selectPointsB->isChecked());
281 
282     // Add 5 points to the region by clicking on the skymap.
283     skyClick(skymap, 200, 200);
284     skyClick(skymap, 250, 250);
285     skyClick(skymap, 300, 300);
286     skyClick(skymap, 350, 350);
287     skyClick(skymap, 400, 400);
288 
289     // Make sure there are 5 points now for region 0.
290     QVERIFY(5 == getRegion(0)->rowCount());
291     QVERIFY(checkForRepeatedAzAlt(0));
292 
293     // Turn this region into a ceiling, check it was noted, and turn that off.
294     QVERIFY(!getRegion(0)->data(Qt::UserRole).toBool());
295     KTRY_AH_CLICK(toggleCeilingB);
296     QVERIFY(getRegion(0)->data(Qt::UserRole).toBool());
297     KTRY_AH_CLICK(toggleCeilingB);
298 
299     // Add a 2nd region. This also turns of mouse-entry of points.
300     KTRY_AH_CLICK(addRegionB);
301     QVERIFY(!selectPointsB->isChecked());
302 
303     // The new region shouldn't have any points, but the first should still have 5.
304     QVERIFY(5 == getRegion(0)->rowCount());
305     QVERIFY(0 == getRegion(1)->rowCount());
306 
307     // Add 2 points to the 2nd region.
308     KTRY_AH_CLICK(selectPointsB);
309     skyClick(skymap, 400, 400);
310     skyClick(skymap, 450, 450);
311     QVERIFY(5 == getRegion(0)->rowCount());
312     QVERIFY(2 == getRegion(1)->rowCount());
313     QVERIFY(checkForRepeatedAzAlt(0));
314     QVERIFY(checkForRepeatedAzAlt(1));
315 
316     // Make sure the live preview reflects the points in the 2nd region.
317     QVERIFY(horizonComponent->livePreview.get());
318     QVERIFY(compareLivePreview(1, horizonComponent->livePreview->points()));
319 
320     // The 2nd region should still be the active one.
321     QVERIFY(1 == regionsList->currentIndex().row());
322 
323     // Select the first region, and make sure it becomes active.
324     QVERIFY(clickSelectRegion(regionsList, 0));
325     QVERIFY(0 == regionsList->currentIndex().row());
326 
327     // Keep these points for later comparison.
328     QList<SkyPoint> pointsA = getRegionPoints(0);
329 
330     // Click on the skymap to add a new point to the first region.
331     skyClick(skymap, 450, 450);
332     // The 1st region should now have one more point.
333     QVERIFY(6 == getRegion(0)->rowCount());
334     QVERIFY(2 == getRegion(1)->rowCount());
335 
336     // The point should have been appended to the end of the 1st region.
337     QList<SkyPoint> pointsB = getRegionPoints(0);
338     QVERIFY(pointsA.size() + 1 == pointsB.size());
339     for (int i = 0; i < pointsA.size(); i++)
340         QVERIFY(pointsA[i] == pointsB[i]);
341 
342     QVERIFY(checkForRepeatedAzAlt(0));
343     QVERIFY(checkForRepeatedAzAlt(1));
344 
345     // Make sure the live preview now reflects the points in the 1st region.
346     QVERIFY(horizonComponent->livePreview.get());
347     QVERIFY(compareLivePreview(0, horizonComponent->livePreview->points()));
348 
349     // Select the 3rd point in the 1st region
350     QVERIFY(clickSelectPoint(pointsList, 0, 2));
351     QVERIFY(2 == pointsList->currentIndex().row());
352 
353     // Copy the points for later comparison.
354     pointsA = getRegionPoints(0);
355 
356     // Insert a point after the (just selected) 3rd point by clicking on the SkyMap.
357     skyClick(skymap, 375, 330);
358     QVERIFY(7 == getRegion(0)->rowCount());
359     QVERIFY(2 == getRegion(1)->rowCount());
360 
361     // The new point should have been place in the middle.
362     pointsB = getRegionPoints(0);
363     QVERIFY(pointsA.size() + 1 == pointsB.size());
364     for (int i = 0; i < 3; i++)
365         QVERIFY(pointsA[i] == pointsB[i]);
366     for (int i = 3; i < pointsA.size(); i++)
367         QVERIFY(pointsA[i] == pointsB[i + 1]);
368 
369     QVERIFY(checkForRepeatedAzAlt(0));
370     QVERIFY(checkForRepeatedAzAlt(1));
371 
372     // Select the 5th point in the 1st region and delete it.
373     QVERIFY(clickSelectPoint(pointsList, 0, 4));
374     QVERIFY(4 == pointsList->currentIndex().row());
375     KTRY_AH_CLICK(removePointB);
376 
377     // There should now be one less point in the 1st region.
378     QVERIFY(6 == getRegion(0)->rowCount());
379     QVERIFY(2 == getRegion(1)->rowCount());
380 
381     QVERIFY(checkForRepeatedAzAlt(0));
382     QVERIFY(checkForRepeatedAzAlt(1));
383 
384     // Clear all the points in the (currently selected) 1st region.
385     KTRY_AH_CLICK(clearPointsB);
386     QVERIFY(0 == getRegion(0)->rowCount());
387     QVERIFY(2 == getRegion(1)->rowCount());
388 
389     // Remove the original 1st region. The 2nd region becomes the 1st one.
390     pointsA = getRegionPoints(1);
391     KTRY_AH_CLICK(removeRegionB);
392     QVERIFY(2 == getRegion(0)->rowCount());
393     pointsB = getRegionPoints(0);
394     QVERIFY(pointsA == pointsB);
395 
396     QVERIFY(checkForRepeatedAzAlt(0));
397 
398     // Apply, then close the Artificial Horizon menu.
399 
400     // Equivalent to clicking the apply button.
401     KStars::Instance()->m_HorizonManager->slotSaveChanges();
402     // same as clicking X to close the window.
403     KStars::Instance()->m_HorizonManager->close();
404 
405     // Should no longer have a live preview.
406     QVERIFY(!horizonComponent->livePreview.get());
407 
408     // Re-open the menu.
409     KStars::Instance()->slotHorizonManager();
410 
411     // The 2-point region should still be there.
412     QVERIFY(regionsList->model()->rowCount() == 1);
413     QVERIFY(2 == getRegion(0)->rowCount());
414 
415     // There should be a live preview again.
416     QVERIFY(horizonComponent->livePreview.get());
417     QVERIFY(compareLivePreview(0, horizonComponent->livePreview->points()));
418 
419     // This section tests to make sure that, when a region is enabled,
420     // the horizon component's isVisible() method reflects the values
421     // of the region. Will test using the 2 points of the saved region above.
422 
423     // Get the approximate azimuth and altitude at the midpoint of the 2-point region.
424     pointsA = getRegionPoints(0);
425     QVERIFY(2 == pointsA.size());
426     const double az = (pointsA[0].az().Degrees() + pointsA[1].az().Degrees()) / 2.0;
427     const double alt = (pointsA[0].alt().Degrees() + pointsA[1].alt().Degrees()) / 2.0;
428 
429     // Make sure the region is enabled.
430     const auto state = getRegion(0)->checkState();
431     if (state != Qt::Checked)
432     {
433         QVERIFY(clickEnableRegion(regionsList, 0));
434         QVERIFY(getRegion(0)->checkState() == Qt::Checked);
435     }
436 
437     // Same as clicking the apply button.
438     KStars::Instance()->m_HorizonManager->slotSaveChanges();
439 
440     // Verify that isVisible() roughly reflects the region's limit.
441     QVERIFY(horizonComponent->getHorizon().isVisible(az, alt + 5));
442     QVERIFY(!horizonComponent->getHorizon().isVisible(az, alt - 5));
443 
444     // Turn the line into a ceiling line. The visibility should be reversed.
445     KTRY_AH_CLICK(toggleCeilingB);
446     KStars::Instance()->m_HorizonManager->slotSaveChanges();
447     QVERIFY(!horizonComponent->getHorizon().isVisible(az, alt + 5));
448     QVERIFY(horizonComponent->getHorizon().isVisible(az, alt - 5));
449     // and turn ceiling off for that line, and the original visibility returns.
450     KTRY_AH_CLICK(toggleCeilingB);
451     KStars::Instance()->m_HorizonManager->slotSaveChanges();
452     QVERIFY(horizonComponent->getHorizon().isVisible(az, alt + 5));
453     QVERIFY(!horizonComponent->getHorizon().isVisible(az, alt - 5));
454 
455     // Now Disable the constraint.
456     QVERIFY(clickEnableRegion(regionsList, 0));
457     QVERIFY(getRegion(0)->checkState() == Qt::Unchecked);
458     // Same as clicking the apply button.
459     KStars::Instance()->m_HorizonManager->slotSaveChanges();
460     // The constraint should be at -90 when not enabled.
461     QVERIFY(horizonComponent->getHorizon().isVisible(az, -89));
462 
463     // Finally enable the region again, click apply, and exit the module.
464     // The constraint should still be there, even with the menu closed.
465     QVERIFY(clickEnableRegion(regionsList, 0));
466     QVERIFY(getRegion(0)->checkState() == Qt::Checked);
467     KStars::Instance()->m_HorizonManager->slotSaveChanges();
468     KStars::Instance()->m_HorizonManager->close();
469     QVERIFY(horizonComponent->getHorizon().isVisible(az, alt + 5));
470     QVERIFY(!horizonComponent->getHorizon().isVisible(az, alt - 5));
471 }
472 
473 QTEST_KSTARS_MAIN(TestArtificialHorizon)
474 
475 #endif // HAVE_INDI
476