File indexing completed on 2024-04-21 14:47:20

0001 /*  Artificial Horizon UI test
0002     SPDX-FileCopyrightText: 2021 Hy Murveit <hy@murveit.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "test_artificial_horizon.h"
0008 
0009 #if defined(HAVE_INDI)
0010 
0011 #include <QStandardItemModel>
0012 
0013 #include "artificialhorizoncomponent.h"
0014 #include "kstars_ui_tests.h"
0015 #include "horizonmanager.h"
0016 #include "linelist.h"
0017 #include "skycomponents/skymapcomposite.h"
0018 #include "skymap.h"
0019 #include "test_ekos.h"
0020 
0021 TestArtificialHorizon::TestArtificialHorizon(QObject *parent) : QObject(parent)
0022 {
0023 }
0024 
0025 void TestArtificialHorizon::initTestCase()
0026 {
0027     // HACK: Reset clock to initial conditions
0028     KHACK_RESET_EKOS_TIME();
0029 }
0030 
0031 void TestArtificialHorizon::cleanupTestCase()
0032 {
0033 }
0034 
0035 void TestArtificialHorizon::init()
0036 {
0037 
0038 }
0039 
0040 void TestArtificialHorizon::cleanup()
0041 {
0042 
0043 }
0044 
0045 void TestArtificialHorizon::testArtificialHorizon_data()
0046 {
0047 
0048 }
0049 
0050 namespace
0051 {
0052 
0053 void skyClick(SkyMap *sky, int x, int y)
0054 {
0055     QTest::mouseClick(sky->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(x, y), 100);
0056 }
0057 
0058 // Returns the QModelIndex for the nth point in the mth region
0059 QModelIndex pointIndex(QStandardItemModel *model, int region, int point)
0060 {
0061     QModelIndex badIndex;
0062     if (model == nullptr)
0063         return badIndex;
0064     auto reg = model->item(region, 0);
0065     if (reg == nullptr)
0066         return badIndex;
0067     auto child = reg->child(point, 1);
0068     if (child == nullptr)
0069         return badIndex;
0070     return child->index();
0071 }
0072 
0073 // Returns the QModelIndex for the nth region .
0074 QModelIndex regionIndex(QStandardItemModel *model, int region)
0075 {
0076     QModelIndex badIndex;
0077     if (model == nullptr)
0078         return badIndex;
0079     QModelIndex idx = model->index(region, 0);
0080     return idx;
0081 }
0082 
0083 // Simulates a left mouse clock on the given view.
0084 // The click is centered vertically, and offset by leftOffset from the left size of the view.
0085 bool clickView(QAbstractItemView *view, const QModelIndex &idx, int leftOffset)
0086 {
0087     if (!idx.isValid())
0088         return false;
0089     QPoint itemPtCenter = view->visualRect(idx).center();
0090     if (itemPtCenter.isNull())
0091         return false;
0092     QPoint itemPtLeft = itemPtCenter;
0093     itemPtLeft.setX(view->visualRect(idx).left() + leftOffset);
0094     QTest::mouseClick(view->viewport(), Qt::LeftButton, Qt::NoModifier, itemPtLeft);
0095     return true;
0096 }
0097 
0098 // Simulates a click on the enable checkbox for the region view.
0099 bool clickEnableRegion(QAbstractItemView *view, int region)
0100 {
0101     QStandardItemModel *model = static_cast<QStandardItemModel*>(view->model());
0102     const QModelIndex index = regionIndex(model, region);
0103     if (!index.isValid()) return false;
0104     // The checkbox is on the far left, so left offset is 5.
0105     return clickView(view, index, 5);
0106 }
0107 
0108 // Simulates a click on the nth region, in the region view.
0109 bool clickSelectRegion(QAbstractItemView *view, int region)
0110 {
0111     QStandardItemModel *model = static_cast<QStandardItemModel*>(view->model());
0112     const QModelIndex index = regionIndex(model, region);
0113     if (!index.isValid()) return false;
0114     // Clicks near the middle of the box (left offset of 50).
0115     return clickView(view, index, 50);
0116 }
0117 
0118 // Simulates a click on the nth point, in the point list view.
0119 bool clickSelectPoint(QAbstractItemView *view, int region, int point)
0120 {
0121     QStandardItemModel *model = static_cast<QStandardItemModel*>(view->model());
0122     const QModelIndex index = pointIndex(model, region, point);
0123     // Clicks near the middle of he box (left offset of 50).
0124     return clickView(view, index, 50);
0125 }
0126 
0127 #if 0
0128 // Debugging printout. Prints the list of az/alt points in a region.
0129 bool printAzAlt(QStandardItemModel *model, int region)
0130 {
0131     if (model->rowCount() <= region)
0132         return false;
0133 
0134     const auto reg = model->item(region);
0135     const int numPoints = reg->rowCount();
0136     for (int i = 0; i < numPoints; ++i)
0137     {
0138         QStandardItem *azItem  = reg->child(i, 1);
0139         QStandardItem *altItem = reg->child(i, 2);
0140 
0141         const dms az  = dms::fromString(azItem->data(Qt::DisplayRole).toString(), true);
0142         const dms alt = dms::fromString(altItem->data(Qt::DisplayRole).toString(), true);
0143 
0144         fprintf(stderr, "az %f alt %f\n", az.Degrees(), alt.Degrees());
0145     }
0146     return true;
0147 }
0148 #endif
0149 
0150 }  // namespace
0151 
0152 // Returns the nth region.
0153 QStandardItem *TestArtificialHorizon::getRegion(int region)
0154 {
0155     return m_Model->item(region);
0156 }
0157 
0158 
0159 // Creates a list of SkyPoints corresponding to the points in the nth region.
0160 QList<SkyPoint> TestArtificialHorizon::getRegionPoints(int region)
0161 {
0162     const auto reg = getRegion(region);
0163     const int numPoints = reg->rowCount();
0164     QList<SkyPoint> pts;
0165     for (int i = 0; i < numPoints; ++i)
0166     {
0167         QStandardItem *azItem  = reg->child(i, 1);
0168         QStandardItem *altItem = reg->child(i, 2);
0169         const dms az  = dms::fromString(azItem->data(Qt::DisplayRole).toString(), true);
0170         const dms alt = dms::fromString(altItem->data(Qt::DisplayRole).toString(), true);
0171         SkyPoint p;
0172         p.setAz(az);
0173         p.setAlt(alt);
0174         pts.append(p);
0175     }
0176     return pts;
0177 }
0178 
0179 // Returns true if the SkyList contains the same az/alt points as the region.
0180 bool TestArtificialHorizon::compareLivePreview(int region, SkyList *previewPoints)
0181 {
0182     if (m_Model->rowCount() <= region)
0183         return false;
0184     QList<SkyPoint> regionPoints = getRegionPoints(region);
0185     if (previewPoints->size() != regionPoints.size())
0186         return false;
0187     for (int i = 0; i < regionPoints.size(); ++i)
0188     {
0189         if (previewPoints->at(i)->az().Degrees() != regionPoints[i].az().Degrees() ||
0190                 previewPoints->at(i)->alt().Degrees() != regionPoints[i].alt().Degrees())
0191             return false;
0192     }
0193     return true;
0194 }
0195 
0196 // Checks for a testing bug where all az/alt points were repeated.
0197 bool TestArtificialHorizon::checkForRepeatedAzAlt(int region)
0198 {
0199     if (m_Model->rowCount() <= region)
0200         return false;
0201     const auto reg = getRegion(region);
0202     const int numPoints = reg->rowCount();
0203     double azKeep = 0, altKeep = 0;
0204     for (int i = 0; i < numPoints; ++i)
0205     {
0206         QStandardItem *azItem  = reg->child(i, 1);
0207         QStandardItem *altItem = reg->child(i, 2);
0208 
0209         const dms az  = dms::fromString(azItem->data(Qt::DisplayRole).toString(), true);
0210         const dms alt = dms::fromString(altItem->data(Qt::DisplayRole).toString(), true);
0211 
0212         if (i == 0)
0213         {
0214             azKeep = az.Degrees();
0215             altKeep = alt.Degrees();
0216         }
0217         else
0218         {
0219             if (az.Degrees() == azKeep || altKeep == alt.Degrees())
0220             {
0221                 fprintf(stderr, "Repeated point in Region %d pt %d: %f %f\n", region, i, az.Degrees(), alt.Degrees());
0222                 return false;
0223             }
0224         }
0225     }
0226     return true;
0227 }
0228 
0229 void TestArtificialHorizon::testArtificialHorizon()
0230 {
0231     // Open the Artificial Horizon menu and instantiate the interface.
0232     KStars::Instance()->slotHorizonManager();
0233     SkyMap *skymap = KStars::Instance()->map();
0234 
0235     ArtificialHorizonComponent *horizonComponent = KStarsData::Instance()->skyComposite()->artificialHorizon();
0236 
0237     // Region buttons
0238     KTRY_AH_GADGET(QPushButton, addRegionB);
0239     KTRY_AH_GADGET(QPushButton, removeRegionB);
0240     KTRY_AH_GADGET(QPushButton, toggleCeilingB);
0241     KTRY_AH_GADGET(QPushButton, saveB);
0242     // Points buttons
0243     KTRY_AH_GADGET(QPushButton, addPointB);
0244     KTRY_AH_GADGET(QPushButton, removePointB);
0245     KTRY_AH_GADGET(QPushButton, clearPointsB);
0246     KTRY_AH_GADGET(QPushButton, selectPointsB);
0247     // Views
0248     KTRY_AH_GADGET(QTableView, pointsList);
0249     KTRY_AH_GADGET(QListView, regionsList);
0250 
0251     // This is the underlying data structure for the entire UI.
0252     m_Model = static_cast<QStandardItemModel*>(regionsList->model());
0253 
0254     // There shouldn't be any regions at the start.
0255     QVERIFY(regionsList->model()->rowCount() == 0);
0256 
0257     // There should be a live preview.
0258     QVERIFY(!horizonComponent->livePreview.get());
0259 
0260     // Add a region.
0261     KTRY_AH_CLICK(addRegionB);
0262 
0263     // There should now be  one region, it has no points, is checked, and named "Region 1".
0264     QVERIFY(regionsList->currentIndex().row() == 0);
0265     QVERIFY(getRegion(0)->rowCount() == 0);
0266     QVERIFY(getRegion(0)->checkState() == Qt::Checked);
0267     QVERIFY(m_Model->index(0, 0).data( Qt::DisplayRole ).toString() == QString("Region 1"));
0268 
0269     // Check we can toggle on and off "enable" with a mouse click.
0270     QVERIFY(clickEnableRegion(regionsList, 0));
0271     QVERIFY(getRegion(0)->checkState() == Qt::Unchecked);
0272     QVERIFY(clickEnableRegion(regionsList, 0));
0273     QVERIFY(getRegion(0)->checkState() == Qt::Checked);
0274 
0275     // Mouse-click entry of points shouldn't be enabled yet.
0276     QVERIFY(!selectPointsB->isChecked());
0277 
0278     // Enable mouse-click entry of points
0279     KTRY_AH_CLICK(selectPointsB);
0280     QVERIFY(selectPointsB->isChecked());
0281 
0282     // Add 5 points to the region by clicking on the skymap.
0283     skyClick(skymap, 200, 200);
0284     skyClick(skymap, 250, 250);
0285     skyClick(skymap, 300, 300);
0286     skyClick(skymap, 350, 350);
0287     skyClick(skymap, 400, 400);
0288 
0289     // Make sure there are 5 points now for region 0.
0290     QVERIFY(5 == getRegion(0)->rowCount());
0291     QVERIFY(checkForRepeatedAzAlt(0));
0292 
0293     // Turn this region into a ceiling, check it was noted, and turn that off.
0294     QVERIFY(!getRegion(0)->data(Qt::UserRole).toBool());
0295     KTRY_AH_CLICK(toggleCeilingB);
0296     QVERIFY(getRegion(0)->data(Qt::UserRole).toBool());
0297     KTRY_AH_CLICK(toggleCeilingB);
0298 
0299     // Add a 2nd region. This also turns of mouse-entry of points.
0300     KTRY_AH_CLICK(addRegionB);
0301     QVERIFY(!selectPointsB->isChecked());
0302 
0303     // The new region shouldn't have any points, but the first should still have 5.
0304     QVERIFY(5 == getRegion(0)->rowCount());
0305     QVERIFY(0 == getRegion(1)->rowCount());
0306 
0307     // Add 2 points to the 2nd region.
0308     KTRY_AH_CLICK(selectPointsB);
0309     skyClick(skymap, 400, 400);
0310     skyClick(skymap, 450, 450);
0311     QVERIFY(5 == getRegion(0)->rowCount());
0312     QVERIFY(2 == getRegion(1)->rowCount());
0313     QVERIFY(checkForRepeatedAzAlt(0));
0314     QVERIFY(checkForRepeatedAzAlt(1));
0315 
0316     // Make sure the live preview reflects the points in the 2nd region.
0317     QVERIFY(horizonComponent->livePreview.get());
0318     QVERIFY(compareLivePreview(1, horizonComponent->livePreview->points()));
0319 
0320     // The 2nd region should still be the active one.
0321     QVERIFY(1 == regionsList->currentIndex().row());
0322 
0323     // Select the first region, and make sure it becomes active.
0324     QVERIFY(clickSelectRegion(regionsList, 0));
0325     QVERIFY(0 == regionsList->currentIndex().row());
0326 
0327     // Keep these points for later comparison.
0328     QList<SkyPoint> pointsA = getRegionPoints(0);
0329 
0330     // Click on the skymap to add a new point to the first region.
0331     skyClick(skymap, 450, 450);
0332     // The 1st region should now have one more point.
0333     QVERIFY(6 == getRegion(0)->rowCount());
0334     QVERIFY(2 == getRegion(1)->rowCount());
0335 
0336     // The point should have been appended to the end of the 1st region.
0337     QList<SkyPoint> pointsB = getRegionPoints(0);
0338     QVERIFY(pointsA.size() + 1 == pointsB.size());
0339     for (int i = 0; i < pointsA.size(); i++)
0340         QVERIFY(pointsA[i] == pointsB[i]);
0341 
0342     QVERIFY(checkForRepeatedAzAlt(0));
0343     QVERIFY(checkForRepeatedAzAlt(1));
0344 
0345     // Make sure the live preview now reflects the points in the 1st region.
0346     QVERIFY(horizonComponent->livePreview.get());
0347     QVERIFY(compareLivePreview(0, horizonComponent->livePreview->points()));
0348 
0349     // Select the 3rd point in the 1st region
0350     QVERIFY(clickSelectPoint(pointsList, 0, 2));
0351     QVERIFY(2 == pointsList->currentIndex().row());
0352 
0353     // Copy the points for later comparison.
0354     pointsA = getRegionPoints(0);
0355 
0356     // Insert a point after the (just selected) 3rd point by clicking on the SkyMap.
0357     skyClick(skymap, 375, 330);
0358     QVERIFY(7 == getRegion(0)->rowCount());
0359     QVERIFY(2 == getRegion(1)->rowCount());
0360 
0361     // The new point should have been place in the middle.
0362     pointsB = getRegionPoints(0);
0363     QVERIFY(pointsA.size() + 1 == pointsB.size());
0364     for (int i = 0; i < 3; i++)
0365         QVERIFY(pointsA[i] == pointsB[i]);
0366     for (int i = 3; i < pointsA.size(); i++)
0367         QVERIFY(pointsA[i] == pointsB[i + 1]);
0368 
0369     QVERIFY(checkForRepeatedAzAlt(0));
0370     QVERIFY(checkForRepeatedAzAlt(1));
0371 
0372     // Select the 5th point in the 1st region and delete it.
0373     QVERIFY(clickSelectPoint(pointsList, 0, 4));
0374     QVERIFY(4 == pointsList->currentIndex().row());
0375     KTRY_AH_CLICK(removePointB);
0376 
0377     // There should now be one less point in the 1st region.
0378     QVERIFY(6 == getRegion(0)->rowCount());
0379     QVERIFY(2 == getRegion(1)->rowCount());
0380 
0381     QVERIFY(checkForRepeatedAzAlt(0));
0382     QVERIFY(checkForRepeatedAzAlt(1));
0383 
0384     // Clear all the points in the (currently selected) 1st region.
0385     KTRY_AH_CLICK(clearPointsB);
0386     QVERIFY(0 == getRegion(0)->rowCount());
0387     QVERIFY(2 == getRegion(1)->rowCount());
0388 
0389     // Remove the original 1st region. The 2nd region becomes the 1st one.
0390     pointsA = getRegionPoints(1);
0391     KTRY_AH_CLICK(removeRegionB);
0392     QVERIFY(2 == getRegion(0)->rowCount());
0393     pointsB = getRegionPoints(0);
0394     QVERIFY(pointsA == pointsB);
0395 
0396     QVERIFY(checkForRepeatedAzAlt(0));
0397 
0398     // Apply, then close the Artificial Horizon menu.
0399 
0400     // Equivalent to clicking the apply button.
0401     KStars::Instance()->m_HorizonManager->slotSaveChanges();
0402     // same as clicking X to close the window.
0403     KStars::Instance()->m_HorizonManager->close();
0404 
0405     // Should no longer have a live preview.
0406     QVERIFY(!horizonComponent->livePreview.get());
0407 
0408     // Re-open the menu.
0409     KStars::Instance()->slotHorizonManager();
0410 
0411     // The 2-point region should still be there.
0412     QVERIFY(regionsList->model()->rowCount() == 1);
0413     QVERIFY(2 == getRegion(0)->rowCount());
0414 
0415     // There should be a live preview again.
0416     QVERIFY(horizonComponent->livePreview.get());
0417     QVERIFY(compareLivePreview(0, horizonComponent->livePreview->points()));
0418 
0419     // This section tests to make sure that, when a region is enabled,
0420     // the horizon component's isVisible() method reflects the values
0421     // of the region. Will test using the 2 points of the saved region above.
0422 
0423     // Get the approximate azimuth and altitude at the midpoint of the 2-point region.
0424     pointsA = getRegionPoints(0);
0425     QVERIFY(2 == pointsA.size());
0426     const double az = (pointsA[0].az().Degrees() + pointsA[1].az().Degrees()) / 2.0;
0427     const double alt = (pointsA[0].alt().Degrees() + pointsA[1].alt().Degrees()) / 2.0;
0428 
0429     // Make sure the region is enabled.
0430     const auto state = getRegion(0)->checkState();
0431     if (state != Qt::Checked)
0432     {
0433         QVERIFY(clickEnableRegion(regionsList, 0));
0434         QVERIFY(getRegion(0)->checkState() == Qt::Checked);
0435     }
0436 
0437     // Same as clicking the apply button.
0438     KStars::Instance()->m_HorizonManager->slotSaveChanges();
0439 
0440     // Verify that isVisible() roughly reflects the region's limit.
0441     QVERIFY(horizonComponent->getHorizon().isVisible(az, alt + 5));
0442     QVERIFY(!horizonComponent->getHorizon().isVisible(az, alt - 5));
0443 
0444     // Turn the line into a ceiling line. The visibility should be reversed.
0445     KTRY_AH_CLICK(toggleCeilingB);
0446     KStars::Instance()->m_HorizonManager->slotSaveChanges();
0447     QVERIFY(!horizonComponent->getHorizon().isVisible(az, alt + 5));
0448     QVERIFY(horizonComponent->getHorizon().isVisible(az, alt - 5));
0449     // and turn ceiling off for that line, and the original visibility returns.
0450     KTRY_AH_CLICK(toggleCeilingB);
0451     KStars::Instance()->m_HorizonManager->slotSaveChanges();
0452     QVERIFY(horizonComponent->getHorizon().isVisible(az, alt + 5));
0453     QVERIFY(!horizonComponent->getHorizon().isVisible(az, alt - 5));
0454 
0455     // Now Disable the constraint.
0456     QVERIFY(clickEnableRegion(regionsList, 0));
0457     QVERIFY(getRegion(0)->checkState() == Qt::Unchecked);
0458     // Same as clicking the apply button.
0459     KStars::Instance()->m_HorizonManager->slotSaveChanges();
0460     // The constraint should be at -90 when not enabled.
0461     QVERIFY(horizonComponent->getHorizon().isVisible(az, -89));
0462 
0463     // Finally enable the region again, click apply, and exit the module.
0464     // The constraint should still be there, even with the menu closed.
0465     QVERIFY(clickEnableRegion(regionsList, 0));
0466     QVERIFY(getRegion(0)->checkState() == Qt::Checked);
0467     KStars::Instance()->m_HorizonManager->slotSaveChanges();
0468     KStars::Instance()->m_HorizonManager->close();
0469     QVERIFY(horizonComponent->getHorizon().isVisible(az, alt + 5));
0470     QVERIFY(!horizonComponent->getHorizon().isVisible(az, alt - 5));
0471 }
0472 
0473 QTEST_KSTARS_MAIN(TestArtificialHorizon)
0474 
0475 #endif // HAVE_INDI