File indexing completed on 2024-04-14 14:11:19

0001 /*
0002     SPDX-FileCopyrightText: 2015 Jasem Mutlaq <mutlaqja@ikarustech.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "artificialhorizoncomponent.h"
0008 
0009 #include "greatcircle.h"
0010 #include "kstarsdata.h"
0011 #include "linelist.h"
0012 #include "Options.h"
0013 #include "skymap.h"
0014 #include "skymapcomposite.h"
0015 #include "skypainter.h"
0016 #include "projections/projector.h"
0017 
0018 #define UNDEFINED_ALTITUDE -90
0019 
0020 ArtificialHorizonEntity::~ArtificialHorizonEntity()
0021 {
0022     clearList();
0023 }
0024 
0025 QString ArtificialHorizonEntity::region() const
0026 {
0027     return m_Region;
0028 }
0029 
0030 void ArtificialHorizonEntity::setRegion(const QString &Region)
0031 {
0032     m_Region = Region;
0033 }
0034 
0035 bool ArtificialHorizonEntity::enabled() const
0036 {
0037     return m_Enabled;
0038 }
0039 
0040 void ArtificialHorizonEntity::setEnabled(bool Enabled)
0041 {
0042     m_Enabled = Enabled;
0043 }
0044 
0045 bool ArtificialHorizonEntity::ceiling() const
0046 {
0047     return m_Ceiling;
0048 }
0049 
0050 void ArtificialHorizonEntity::setCeiling(bool value)
0051 {
0052     m_Ceiling = value;
0053 }
0054 
0055 void ArtificialHorizonEntity::setList(const std::shared_ptr<LineList> &list)
0056 {
0057     m_List = list;
0058 }
0059 
0060 std::shared_ptr<LineList> ArtificialHorizonEntity::list() const
0061 {
0062     return m_List;
0063 }
0064 
0065 void ArtificialHorizonEntity::clearList()
0066 {
0067     m_List.reset();
0068 }
0069 
0070 namespace
0071 {
0072 
0073 // Returns true if angle is "in between" range1 and range2, two other angles,
0074 // where in-between means "the short way".
0075 bool inBetween(const dms &angle, const dms &range1, const dms &range2)
0076 {
0077     const double rangeDelta = fabs(range1.deltaAngle(range2).Degrees());
0078     const double delta1 = fabs(range1.deltaAngle(angle).Degrees());
0079     const double delta2 = fabs(range2.deltaAngle(angle).Degrees());
0080     // The angle is between range1 and range2 if its two distances to each are both
0081     // less than the range distance.
0082     return delta1 <= rangeDelta && delta2 <= rangeDelta;
0083 }
0084 }  // namespace
0085 
0086 double ArtificialHorizonEntity::altitudeConstraint(double azimuthDegrees, bool *constraintExists) const
0087 {
0088     *constraintExists = false;
0089     if (m_List == nullptr)
0090         return UNDEFINED_ALTITUDE;
0091 
0092     SkyList *points = m_List->points();
0093     if (points == nullptr)
0094         return UNDEFINED_ALTITUDE;
0095 
0096     double constraint = !m_Ceiling ? UNDEFINED_ALTITUDE : 90.0;
0097     dms desiredAzimuth(azimuthDegrees);
0098     dms lastAz;
0099     double lastAlt = 0;
0100     bool firstOne = true;
0101     for (auto &p : *points)
0102     {
0103         const dms az = p->az();
0104         const double alt = p->alt().Degrees();
0105         if (qIsNaN(az.Degrees()) || qIsNaN(alt)) continue;
0106         if (!firstOne && inBetween(desiredAzimuth, lastAz, az))
0107         {
0108             *constraintExists = true;
0109             // If the input angle is in the interval between the last two points,
0110             // interpolate the altitude constraint, and use that value.
0111             // If there are other line segments which also contain the point,
0112             // we use the max constraint. Convert to GreatCircle?
0113             const double totalDelta = fabs(lastAz.deltaAngle(az).Degrees());
0114             if (totalDelta <= 0)
0115             {
0116                 if (!m_Ceiling)
0117                     constraint = std::max(constraint, alt);
0118                 else
0119                     constraint = std::min(constraint, alt);
0120             }
0121             else
0122             {
0123                 const double deltaToLast = fabs(lastAz.deltaAngle(desiredAzimuth).Degrees());
0124                 const double weight = deltaToLast / totalDelta;
0125                 const double newConstraint = (1.0 - weight) * lastAlt + weight * alt;
0126                 if (!m_Ceiling)
0127                     constraint = std::max(constraint, newConstraint);
0128                 else
0129                     constraint = std::min(constraint, newConstraint);
0130             }
0131         }
0132         firstOne = false;
0133         lastAz = az;
0134         lastAlt = alt;
0135     }
0136     return constraint;
0137 }
0138 
0139 ArtificialHorizonComponent::ArtificialHorizonComponent(SkyComposite *parent)
0140     : NoPrecessIndex(parent, i18n("Artificial Horizon"))
0141 {
0142     load();
0143 }
0144 
0145 ArtificialHorizonComponent::~ArtificialHorizonComponent()
0146 {
0147 }
0148 
0149 ArtificialHorizon::~ArtificialHorizon()
0150 {
0151     qDeleteAll(m_HorizonList);
0152     m_HorizonList.clear();
0153 }
0154 
0155 void ArtificialHorizon::load(const QList<ArtificialHorizonEntity *> &list)
0156 {
0157     m_HorizonList = list;
0158     resetPrecomputeConstraints();
0159     checkForCeilings();
0160 }
0161 
0162 bool ArtificialHorizonComponent::load()
0163 {
0164     QList<ArtificialHorizonEntity *> list;
0165     KStarsData::Instance()->userdb()->GetAllHorizons(list);
0166     horizon.load(list);
0167 
0168     foreach (ArtificialHorizonEntity *horizon, *horizon.horizonList())
0169         appendLine(horizon->list());
0170 
0171     return true;
0172 }
0173 
0174 void ArtificialHorizonComponent::save()
0175 {
0176     KStarsData::Instance()->userdb()->DeleteAllHorizons();
0177 
0178     foreach (ArtificialHorizonEntity *horizon, *horizon.horizonList())
0179         KStarsData::Instance()->userdb()->AddHorizon(horizon);
0180 }
0181 
0182 bool ArtificialHorizonComponent::selected()
0183 {
0184     return Options::showGround();
0185 }
0186 
0187 void ArtificialHorizonComponent::preDraw(SkyPainter *skyp)
0188 {
0189     QColor color(KStarsData::Instance()->colorScheme()->colorNamed("ArtificialHorizonColor"));
0190     color.setAlpha(40);
0191     skyp->setBrush(QBrush(color));
0192     skyp->setPen(Qt::NoPen);
0193 }
0194 
0195 namespace
0196 {
0197 
0198 // Returns an equivalent degrees in the range 0 <= 0 < 360
0199 double normalizeDegrees(double degrees)
0200 {
0201     while (degrees < 0)
0202         degrees += 360;
0203     while (degrees >= 360.0)
0204         degrees -= 360.0;
0205     return degrees;
0206 }
0207 
0208 // Draws a "round polygon", sampling a circle every 45 degrees, with the given radius,
0209 // centered on the SkyPoint.
0210 void drawHorizonPoint(const SkyPoint &pt, double radius, SkyPainter *painter)
0211 
0212 {
0213     LineList region;
0214     double az = pt.az().Degrees(), alt = pt.alt().Degrees();
0215 
0216     for (double angle = 0; angle < 360; angle += 45)
0217     {
0218         double radians = angle * 2 * M_PI / 360.0;
0219         double az1 = az + radius * cos(radians);
0220         double alt1 = alt + radius * sin(radians);
0221         std::shared_ptr<SkyPoint> sp(new SkyPoint());
0222         sp->setAz(az1);
0223         sp->setAlt(alt1);
0224         sp->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
0225         region.append(sp);
0226     }
0227     // Repeat the first point.
0228     double az1 = az + radius * cos(0);
0229     double alt1 = alt + radius * sin(0);
0230     std::shared_ptr<SkyPoint> sp(new SkyPoint());
0231     sp->setAz(az1);
0232     sp->setAlt(alt1);
0233     sp->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
0234     region.append(sp);
0235 
0236     painter->drawSkyPolygon(&region, false);
0237 }
0238 
0239 // Draws a series of points whose coordinates are given by the LineList.
0240 void drawHorizonPoints(LineList *lineList, SkyPainter *painter)
0241 {
0242     const SkyList &points = *(lineList->points());
0243     for (int i = 0; i < points.size(); ++i)
0244     {
0245         const SkyPoint &pt = *points[i];
0246         if (qIsNaN(pt.az().Degrees()) || qIsNaN(pt.alt().Degrees()))
0247             continue;
0248         drawHorizonPoint(pt, .5, painter);
0249     }
0250 }
0251 
0252 // Draws a points that is larger than the one drawn by drawHorizonPoint().
0253 // The point's coordinates are the ith (index) point in the LineList.
0254 void drawSelectedPoint(LineList *lineList, int index, SkyPainter *painter)
0255 {
0256     if (index >= 0 && index < lineList->points()->size())
0257     {
0258         const SkyList &points = *(lineList->points());
0259         const SkyPoint &pt = *points[index];
0260         if (qIsNaN(pt.az().Degrees()) || qIsNaN(pt.alt().Degrees()))
0261             return;
0262         drawHorizonPoint(pt, 1.0, painter);
0263     }
0264 }
0265 
0266 // This creates a set of connected line segments from az1,alt1 to az2,alt2, sampling
0267 // points on the great circle between az1,alt1 and az2,alt2 every 2 degrees or so.
0268 // The errors would be obvious for longer lines if we just drew a standard line.
0269 // If testing is true, HorizontalToEquatorial is not called.
0270 void appendGreatCirclePoints(double az1, double alt1, double az2, double alt2, LineList *region, bool testing)
0271 {
0272     constexpr double sampling = 2.0;  // degrees
0273     const double maxAngleDiff = std::max(fabs(az1 - az2), fabs(alt1 - alt2));
0274     const int numSamples = maxAngleDiff / sampling;
0275 
0276     // Hy 9/25/22: These 4 lines cause rendering issues in equatorial mode (horizon mode is ok).
0277     // Not sure why--though I suspect the initial conditions computed in drawPolygons().
0278     // Without them there are some jagged lines near the horizon, but much better than with them.
0279     // std::shared_ptr<SkyPoint> sp0(new SkyPoint());
0280     // sp0->setAz(az1);
0281     // sp0->setAlt(alt1);
0282     // region->append(sp0);
0283 
0284     if (numSamples > 1)
0285     {
0286         GreatCircle gc(az1, alt1, az2, alt2);
0287         for (int i = 1; i < numSamples; ++i)
0288         {
0289             const double fraction = i / static_cast<double>(numSamples);
0290             double az, alt;
0291             gc.waypoint(fraction, &az, &alt);
0292             std::shared_ptr<SkyPoint> sp(new SkyPoint());
0293             sp->setAz(az);
0294             sp->setAlt(alt);
0295             if (!testing)
0296                 sp->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
0297             region->append(sp);
0298         }
0299     }
0300     std::shared_ptr<SkyPoint> sp(new SkyPoint());
0301     sp->setAz(az2);
0302     sp->setAlt(alt2);
0303     // Is HorizontalToEquatorial necessary in any case?
0304     if (!testing)
0305         sp->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
0306     region->append(sp);
0307 }
0308 
0309 }  // namespace
0310 
0311 // Draws a polygon, where one of the sides is az1,alt1 --> az2,alt2 (except that's implemented as series
0312 // of connected line segments along a great circle).
0313 // It figures out the opposite side depending on the type of the constraint for this entity
0314 // (horizon line or ceiling) and the other contraints that are enabled.
0315 bool ArtificialHorizon::computePolygon(int entity, double az1, double alt1, double az2, double alt2,
0316                                        double sampling, LineList *region)
0317 {
0318     const bool ceiling = horizonList()->at(entity)->ceiling();
0319     const ArtificialHorizonEntity *thisOne = horizonList()->at(entity);
0320     double alt1b = 0, alt2b = 0;
0321     bool exists = false;
0322     LineList left, top, right, bottom;
0323 
0324     double lastAz = az1;
0325     double lastAlt = alt1;
0326     const double azRange = az2 - az1, altRange = alt2 - alt1;
0327 
0328     if (az1 >= az2)
0329         return false;
0330 
0331     if (az1 + sampling > az2)
0332         sampling = (az2 - az1) - 1e-6;
0333 
0334     for (double az = az1 + sampling; az <= az2; az += sampling)
0335     {
0336         // Put it at the end, if we're close.
0337         if (az + sampling > az2)
0338             az = az2;
0339 
0340         double alt = alt1 + altRange * (az - az1) / azRange;
0341         double alt1b = 0, alt2b = 0;
0342 
0343         if (!ceiling)
0344         {
0345             // For standard horizon lines, the polygon is drawn down to the next lower-altitude
0346             // enabled line, or to the horizon if a lower line doesn't exist.
0347             const ArtificialHorizonEntity *constraint = getConstraintBelow(lastAz, lastAlt, thisOne);
0348             if (constraint != nullptr)
0349             {
0350                 double altTemp = constraint->altitudeConstraint(lastAz, &exists);
0351                 if (exists)
0352                     alt1b = altTemp;
0353             }
0354             constraint = getConstraintBelow(az, alt, thisOne);
0355             if (constraint != nullptr)
0356             {
0357                 double altTemp = constraint->altitudeConstraint(az, &exists);
0358                 if (exists)
0359                     alt2b = altTemp;
0360             }
0361             appendGreatCirclePoints(lastAz, lastAlt,   az, alt, &top, testing);
0362             appendGreatCirclePoints(lastAz, alt1b,   az, alt2b, &bottom, testing);
0363         }
0364         else
0365         {
0366             // For ceiling lines, the polygon is drawn up to the next higher-altitude enabled line
0367             // but only if that line is another cieling, otherwise it not drawn at all (because that
0368             // horizon line will do the drawing).
0369             const ArtificialHorizonEntity *constraint = getConstraintAbove(lastAz, lastAlt, thisOne);
0370             alt1b = 90;
0371             alt2b = 90;
0372             if (constraint != nullptr)
0373             {
0374                 if (constraint->ceiling()) return false;
0375                 double altTemp = constraint->altitudeConstraint(lastAz, &exists);
0376                 if (exists) alt1b = altTemp;
0377             }
0378             constraint = getConstraintAbove(az, alt, thisOne);
0379             if (constraint != nullptr)
0380             {
0381                 if (constraint->ceiling()) return false;
0382                 double altTemp = constraint->altitudeConstraint(az, &exists);
0383                 if (exists) alt2b = altTemp;
0384             }
0385             appendGreatCirclePoints(lastAz, lastAlt,   az, alt, &top, testing);
0386             // Note that "bottom" for a ceiling is above.
0387             appendGreatCirclePoints(lastAz, alt1b,   az, alt2b, &bottom, testing);
0388         }
0389         lastAz = az;
0390         lastAlt = alt;
0391     }
0392 
0393     if (!ceiling)
0394     {
0395         // For standard horizon lines, the polygon is drawn down to the next lower-altitude
0396         // enabled line, or to the horizon if a lower line doesn't exist.
0397         const ArtificialHorizonEntity *constraint = getConstraintBelow(az1, alt1, thisOne);
0398         if (constraint != nullptr)
0399         {
0400             double altTemp = constraint->altitudeConstraint(az1, &exists);
0401             if (exists)
0402                 alt1b = altTemp;
0403         }
0404         appendGreatCirclePoints(az1, alt1b,   az1, alt1, &left, testing);
0405 
0406         const ArtificialHorizonEntity *constraint2 = getConstraintBelow(az2, alt2, thisOne);
0407         if (constraint2 != nullptr)
0408         {
0409             double altTemp = constraint2->altitudeConstraint(az2, &exists);
0410             if (exists)
0411                 alt2b = altTemp;
0412         }
0413         appendGreatCirclePoints(az2, alt2,   az2, alt2b, &right, testing);
0414     }
0415     else
0416     {
0417         // For ceiling lines, the polygon is drawn up to the next higher-altitude enabled line
0418         // but only if that line is another cieling, otherwise it not drawn at all (because that
0419         // horizon line will do the drawing).
0420         const ArtificialHorizonEntity *constraint = getConstraintAbove(az1, alt1, thisOne);
0421         alt1b = 90;
0422         alt2b = 90;
0423         if (constraint != nullptr)
0424         {
0425             if (!constraint->ceiling()) return false;
0426             double altTemp = constraint->altitudeConstraint(az1, &exists);
0427             if (exists) alt1b = altTemp;
0428         }
0429         appendGreatCirclePoints(az1, alt1b,   az1, alt1, &left, testing);
0430 
0431         const ArtificialHorizonEntity *constraint2 = getConstraintAbove(az2, alt2, thisOne);
0432         if (constraint2 != nullptr)
0433         {
0434             if (!constraint2->ceiling()) return false;
0435             double altTemp = constraint2->altitudeConstraint(az2, &exists);
0436             if (exists) alt2b = altTemp;
0437         }
0438         appendGreatCirclePoints(az2, alt2,   az2, alt2b, &right, testing);
0439     }
0440 
0441     // Now we have all the sides: left, top, right, bottom, but the order of bottom is reversed.
0442     // Make a polygon with all the points.
0443     for (const auto &p : * (left.points()))
0444         region->append(p);
0445     for (const auto &p : * (top.points()))
0446         region->append(p);
0447     for (const auto &p : * (right.points()))
0448         region->append(p);
0449     for (int i = bottom.points()->size() - 1; i >= 0; i--)
0450         region->append(bottom.points()->at(i));
0451 
0452     return true;
0453 }
0454 
0455 // Draws a series of polygons of width in azimuth of "sampling degrees".
0456 // Drawing a single polygon would have "great-circle issues". This looks a lot better.
0457 // Assumes az1 and az2 in range 0-360 and az1 < az2.
0458 // regions is only not nullptr during testing. In this wasy we can test
0459 // whether the appropriate regions are drawn.
0460 void ArtificialHorizon::drawSampledPolygons(int entity, double az1, double alt1, double az2, double alt2,
0461         double sampling, SkyPainter *painter, QList<LineList> *regions)
0462 {
0463     if (az1 > az2)
0464     {
0465         // Should not generally happen. Possibility e.g. az 0 -> 0.01 in a wrap around.
0466         // OK to ignore.
0467         return;
0468     }
0469 
0470     LineList region;
0471     if (computePolygon(entity, az1, alt1, az2, alt2, sampling, &region))
0472     {
0473         if (painter != nullptr)
0474             painter->drawSkyPolygon(&region, false);
0475         if (regions != nullptr)
0476             regions->append(region);
0477     }
0478 }
0479 
0480 // This draws a series of polygons that fill the area that the horizon entity with index "entity"
0481 // is responsible for.  If that is a horizon line, it draws it down to the horizon, or to the next
0482 // lower line. It draws the polygons one pair of points at a time, and deals with complications
0483 // of when the azimuth angle wraps around 360 degrees.
0484 void ArtificialHorizon::drawPolygons(int entity, SkyPainter *painter, QList<LineList> *regions)
0485 {
0486     const ArtificialHorizonEntity &ah = *(horizonList()->at(entity));
0487     const SkyList &points = *(ah.list()->points());
0488 
0489     // The skylist shouldn't contain NaN values, but, it has in the past,
0490     // and, to be cautious, this checks for them and removes points with NaNs.
0491     int start = 0;
0492     for (; start < points.size(); ++start)
0493     {
0494         const SkyPoint &p = *points[start];
0495         if (!qIsNaN(p.az().Degrees()) && !qIsNaN(p.alt().Degrees()))
0496             break;
0497     }
0498 
0499     for (int i = start + 1; i < points.size(); ++i)
0500     {
0501         const SkyPoint &p2 = *points[i];
0502         if (qIsNaN(p2.az().Degrees()) || qIsNaN(p2.alt().Degrees()))
0503             continue;
0504         const SkyPoint &p1 = *points[start];
0505         start = i;
0506 
0507         const double az1 = normalizeDegrees(p1.az().Degrees());
0508         const double az2 = normalizeDegrees(p2.az().Degrees());
0509 
0510         double minAz, maxAz, minAzAlt, maxAzAlt;
0511         if (az1 < az2)
0512         {
0513             minAz = az1;
0514             minAzAlt = p1.alt().Degrees();
0515             maxAz = az2;
0516             maxAzAlt = p2.alt().Degrees();
0517         }
0518         else
0519         {
0520             minAz = az2;
0521             minAzAlt = p2.alt().Degrees();
0522             maxAz = az1;
0523             maxAzAlt = p1.alt().Degrees();
0524         }
0525         const bool wrapAround = !inBetween(dms((minAz + maxAz) / 2.0), dms(minAz), dms(maxAz));
0526         constexpr double sampling = 1.0;  // Draw a polygon for every degree in Azimuth
0527         if (wrapAround)
0528         {
0529             // We've detected that the line segment crosses 0 degrees.
0530             // Draw one polygon on one side of 0 degrees, and another on the other side.
0531             // Compute the altitude at wrap-around.
0532             const double fraction = fabs(dms(360.0).deltaAngle(dms(maxAz)).Degrees() /
0533                                          p1.az().deltaAngle(p2.az()).Degrees());
0534             const double midAlt = minAzAlt + fraction * (maxAzAlt - minAzAlt);
0535             // Draw polygons form maxAz upto 0 degrees, then again from 0 to minAz.
0536             drawSampledPolygons(entity, maxAz, maxAzAlt, 360, midAlt, sampling, painter, regions);
0537             drawSampledPolygons(entity, 0, midAlt, minAz, minAzAlt, sampling, painter, regions);
0538         }
0539         else
0540         {
0541             // Draw the polygons without wraparound
0542             drawSampledPolygons(entity, minAz, minAzAlt, maxAz, maxAzAlt, sampling, painter, regions);
0543         }
0544     }
0545 }
0546 
0547 void ArtificialHorizon::drawPolygons(SkyPainter *painter, QList<LineList> *regions)
0548 {
0549     for (int i = 0; i < horizonList()->size(); i++)
0550     {
0551         if (enabled(i))
0552             drawPolygons(i, painter, regions);
0553     }
0554 }
0555 
0556 void ArtificialHorizonComponent::draw(SkyPainter *skyp)
0557 {
0558     if (!selected())
0559         return;
0560 
0561     if (livePreview.get())
0562     {
0563         if ((livePreview->points() != nullptr) && (livePreview->points()->size() > 0))
0564         {
0565             // Draws a series of line segments, overlayed by the vertices.
0566             // One vertex (the current selection) is emphasized.
0567             skyp->setPen(QPen(Qt::white, 2));
0568             skyp->drawSkyPolyline(livePreview.get());
0569             skyp->setBrush(QBrush(Qt::yellow));
0570             drawSelectedPoint(livePreview.get(), selectedPreviewPoint, skyp);
0571             skyp->setBrush(QBrush(Qt::red));
0572             drawHorizonPoints(livePreview.get(), skyp);
0573         }
0574     }
0575 
0576     preDraw(skyp);
0577 
0578     QList<LineList> regions;
0579     horizon.drawPolygons(skyp, &regions);
0580 }
0581 
0582 bool ArtificialHorizon::enabled(int i) const
0583 {
0584     return m_HorizonList.at(i)->enabled();
0585 }
0586 
0587 ArtificialHorizonEntity *ArtificialHorizon::findRegion(const QString &regionName)
0588 {
0589     ArtificialHorizonEntity *regionHorizon = nullptr;
0590 
0591     foreach (ArtificialHorizonEntity *horizon, m_HorizonList)
0592     {
0593         if (horizon->region() == regionName)
0594         {
0595             regionHorizon = horizon;
0596             break;
0597         }
0598     }
0599 
0600     return regionHorizon;
0601 }
0602 
0603 void ArtificialHorizon::removeRegion(const QString &regionName, bool lineOnly)
0604 {
0605     ArtificialHorizonEntity *regionHorizon = findRegion(regionName);
0606 
0607     if (regionHorizon == nullptr)
0608         return;
0609 
0610     if (lineOnly)
0611         regionHorizon->clearList();
0612     else
0613     {
0614         m_HorizonList.removeOne(regionHorizon);
0615         delete (regionHorizon);
0616     }
0617     resetPrecomputeConstraints();
0618     checkForCeilings();
0619 }
0620 
0621 void ArtificialHorizonComponent::removeRegion(const QString &regionName, bool lineOnly)
0622 {
0623     ArtificialHorizonEntity *regionHorizon = horizon.findRegion(regionName);
0624     if (regionHorizon != nullptr && regionHorizon->list())
0625         removeLine(regionHorizon->list());
0626     horizon.removeRegion(regionName, lineOnly);
0627 }
0628 
0629 void ArtificialHorizon::checkForCeilings()
0630 {
0631     noCeilingConstraints = true;
0632     for (const auto &r : m_HorizonList)
0633     {
0634         if (r->ceiling() && r->enabled())
0635         {
0636             noCeilingConstraints = false;
0637             break;
0638         }
0639     }
0640 }
0641 
0642 void ArtificialHorizon::addRegion(const QString &regionName, bool enabled, const std::shared_ptr<LineList> &list,
0643                                   bool ceiling)
0644 {
0645     ArtificialHorizonEntity *horizon = new ArtificialHorizonEntity;
0646 
0647     horizon->setRegion(regionName);
0648     horizon->setEnabled(enabled);
0649     horizon->setCeiling(ceiling);
0650     horizon->setList(list);
0651 
0652     m_HorizonList.append(horizon);
0653     resetPrecomputeConstraints();
0654     checkForCeilings();
0655 }
0656 
0657 void ArtificialHorizonComponent::addRegion(const QString &regionName, bool enabled, const std::shared_ptr<LineList> &list,
0658         bool ceiling)
0659 {
0660     horizon.addRegion(regionName, enabled, list, ceiling);
0661     appendLine(list);
0662 }
0663 
0664 bool ArtificialHorizon::altitudeConstraintsExist() const
0665 {
0666     foreach (ArtificialHorizonEntity *horizon, m_HorizonList)
0667     {
0668         if (horizon->enabled())
0669             return true;
0670     }
0671     return false;
0672 }
0673 
0674 const ArtificialHorizonEntity *ArtificialHorizon::getConstraintAbove(double azimuthDegrees, double altitudeDegrees,
0675         const ArtificialHorizonEntity *ignore) const
0676 {
0677     double closestAbove = 1e6;
0678     const ArtificialHorizonEntity *entity = nullptr;
0679 
0680     foreach (ArtificialHorizonEntity *horizon, m_HorizonList)
0681     {
0682         if (!horizon->enabled()) continue;
0683         if (horizon == ignore) continue;
0684         bool constraintExists = false;
0685         double constraint = horizon->altitudeConstraint(azimuthDegrees, &constraintExists);
0686         // This horizon doesn't constrain this azimuth.
0687         if (!constraintExists) continue;
0688 
0689         double altitudeDiff = constraint - altitudeDegrees;
0690         if (altitudeDiff > 0 && constraint < closestAbove)
0691         {
0692             closestAbove = constraint;
0693             entity = horizon;
0694         }
0695     }
0696     return entity;
0697 }
0698 
0699 // Estimate the horizon contraint to .1 degrees.
0700 // This significantly speeds up computation.
0701 constexpr int PRECOMPUTED_RESOLUTION = 10;
0702 
0703 double ArtificialHorizon::altitudeConstraint(double azimuthDegrees) const
0704 {
0705     if (precomputedConstraints.size() != 360 * PRECOMPUTED_RESOLUTION)
0706         precomputeConstraints();
0707     return precomputedConstraint(azimuthDegrees);
0708 }
0709 
0710 double ArtificialHorizon::altitudeConstraintInternal(double azimuthDegrees) const
0711 {
0712     const ArtificialHorizonEntity *horizonBelow = getConstraintBelow(azimuthDegrees, 90.0, nullptr);
0713     if (horizonBelow == nullptr)
0714         return UNDEFINED_ALTITUDE;
0715     bool ignore = false;
0716     return horizonBelow->altitudeConstraint(azimuthDegrees, &ignore);
0717 }
0718 
0719 // Quantize the constraints to within .1 degrees (so there are 360*10=3600
0720 // precomputed values).
0721 void ArtificialHorizon::precomputeConstraints() const
0722 {
0723     precomputedConstraints.clear();
0724     precomputedConstraints.fill(0, 360 * PRECOMPUTED_RESOLUTION);
0725     for (int i = 0; i < 360 * PRECOMPUTED_RESOLUTION; ++i)
0726     {
0727         const double az = i / static_cast<double>(PRECOMPUTED_RESOLUTION);
0728         precomputedConstraints[i] = altitudeConstraintInternal(az);
0729     }
0730 }
0731 
0732 void ArtificialHorizon::resetPrecomputeConstraints() const
0733 {
0734     precomputedConstraints.clear();
0735 }
0736 
0737 double ArtificialHorizon::precomputedConstraint(double azimuth) const
0738 {
0739     constexpr int maxval = 360 * PRECOMPUTED_RESOLUTION;
0740     int index = azimuth * PRECOMPUTED_RESOLUTION + 0.5;
0741     if (index == maxval)
0742         index = 0;
0743     if (index < 0 || index >= precomputedConstraints.size())
0744         return UNDEFINED_ALTITUDE;
0745     return precomputedConstraints[index];
0746 }
0747 
0748 const ArtificialHorizonEntity *ArtificialHorizon::getConstraintBelow(double azimuthDegrees, double altitudeDegrees,
0749         const ArtificialHorizonEntity *ignore) const
0750 {
0751     double closestBelow = -1e6;
0752     const ArtificialHorizonEntity *entity = nullptr;
0753 
0754     foreach (ArtificialHorizonEntity *horizon, m_HorizonList)
0755     {
0756         if (!horizon->enabled()) continue;
0757         if (horizon == ignore) continue;
0758         bool constraintExists = false;
0759         double constraint = horizon->altitudeConstraint(azimuthDegrees, &constraintExists);
0760         // This horizon doesn't constrain this azimuth.
0761         if (!constraintExists) continue;
0762 
0763         double altitudeDiff = constraint - altitudeDegrees;
0764         if (altitudeDiff < 0 && constraint > closestBelow)
0765         {
0766             closestBelow = constraint;
0767             entity = horizon;
0768         }
0769     }
0770     return entity;
0771 }
0772 
0773 bool ArtificialHorizon::isAltitudeOK(double azimuthDegrees, double altitudeDegrees, QString *reason) const
0774 {
0775     if (noCeilingConstraints)
0776     {
0777         const double constraint = altitudeConstraint(azimuthDegrees);
0778         if (altitudeDegrees >= constraint)
0779             return true;
0780         if (reason != nullptr)
0781             *reason = QString("altitude %1 < horizon %2").arg(altitudeDegrees, 0, 'f', 1).arg(constraint, 0, 'f', 1);
0782         return false;
0783     }
0784     else
0785         return isVisible(azimuthDegrees, altitudeDegrees, reason);
0786 }
0787 
0788 // An altitude is blocked (not visible) if either:
0789 // - there are constraints above and the closest above constraint is not a ceiling, or
0790 // - there are constraints below and the closest below constraint is a ceiling.
0791 bool ArtificialHorizon::isVisible(double azimuthDegrees, double altitudeDegrees, QString *reason) const
0792 {
0793     const ArtificialHorizonEntity *above = getConstraintAbove(azimuthDegrees, altitudeDegrees);
0794     if (above != nullptr && !above->ceiling())
0795     {
0796         if (reason != nullptr)
0797         {
0798             bool ignoreMe;
0799             double constraint = above->altitudeConstraint(azimuthDegrees, &ignoreMe);
0800             *reason = QString("altitude %1 < horizon %2").arg(altitudeDegrees, 0, 'f', 1).arg(constraint, 0, 'f', 1);
0801         }
0802         return false;
0803     }
0804     const ArtificialHorizonEntity *below = getConstraintBelow(azimuthDegrees, altitudeDegrees);
0805     if (below != nullptr && below->ceiling())
0806     {
0807         if (reason != nullptr)
0808         {
0809             bool ignoreMe;
0810             double constraint = below->altitudeConstraint(azimuthDegrees, &ignoreMe);
0811             *reason = QString("altitude %1 > ceiling %2").arg(altitudeDegrees, 0, 'f', 1).arg(constraint, 0, 'f', 1);
0812         }
0813         return false;
0814     }
0815     return true;
0816 }