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