File indexing completed on 2025-01-12 09:34:20
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(®ion, 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, ®ion)) 0472 { 0473 if (painter != nullptr) 0474 painter->drawSkyPolygon(®ion, 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, ®ions); 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 ®ionName) 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 ®ionName, 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 ®ionName, 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 ®ionName, 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 ®ionName, 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 }