diff --git a/docs/TAG_ITEMS.md b/docs/TAG_ITEMS.md index d3df2e3f3..e99f7cc92 100644 --- a/docs/TAG_ITEMS.md +++ b/docs/TAG_ITEMS.md @@ -30,4 +30,5 @@ 128 - SELCAL Code 129 - SELCAL Code With Separator 130 - Missed Approach Indicator -131 - Relevant ECFMP Flow Measures +131 - Relevant ECFMP Flow Measures +132 - Glideslope Deviation diff --git a/docs/UserGuide/Features/Features.md b/docs/UserGuide/Features/Features.md index f964f9e3d..17df5ec3e 100644 --- a/docs/UserGuide/Features/Features.md +++ b/docs/UserGuide/Features/Features.md @@ -30,3 +30,4 @@ dynamically to the plugin. - [Electronic Prenotes](PrenoteMessages.md) - [SELCAL Parsing](Selcal.md) - [Approach Sequencing](ApproachSequencer.md) +- [Glideslope Deviation](GlideslopeDeviation.md) diff --git a/docs/UserGuide/Features/GlideslopeDeviation.md b/docs/UserGuide/Features/GlideslopeDeviation.md new file mode 100644 index 000000000..628d644f4 --- /dev/null +++ b/docs/UserGuide/Features/GlideslopeDeviation.md @@ -0,0 +1,11 @@ +# Glideslope Deviation + +The glideslope deviation TAG item displays the aircrafts current deviation from the glideslope. It does this by looking at the aircraft current position +along the localiser (by taking a perpendicular line from the aircraft to the localiser) and then calculating the difference between the aircrafts altitude +and the altitude of the glideslope at that point. + +## TAG Item + +The "Glideslope Deviation" TAG item displays the current deviation from the glideslope in the format of `+/-XXX` where `XXX` is the deviation in feet. It will display in +green when the aircraft is within a few hundred feet of or below the glideslope, and red when the aircraft is above the glideslope. If the aircraft is massively above or below the +glideslope, the deviation will be displayed as `>/<1k`. diff --git a/src/plugin/CMakeLists.txt b/src/plugin/CMakeLists.txt index c4e0a1ebe..8a5c76ddb 100644 --- a/src/plugin/CMakeLists.txt +++ b/src/plugin/CMakeLists.txt @@ -55,7 +55,11 @@ set(src__approach approach/ApproachSequencerDisplayOptions.cpp approach/ApproachSequencerDisplayOptions.h approach/ApproachSequencerDisplayAsrLoader.cpp approach/ApproachSequencerDisplayAsrLoader.h approach/ToggleApproachSequencerDisplay.cpp approach/ToggleApproachSequencerDisplay.h - approach/SequencerAirfieldSelector.cpp approach/SequencerAirfieldSelector.h approach/AircraftSelectionProvider.cpp approach/AircraftSelectionProvider.h approach/TargetSelectorList.cpp approach/TargetSelectorList.h approach/ApproachSpacingCalculator.cpp approach/ApproachSpacingCalculator.h approach/ApproachSequencerOptions.cpp approach/ApproachSequencerOptions.h approach/AirfieldApproachOptions.h approach/ApproachSequencerOptionsLoader.cpp approach/ApproachSequencerOptionsLoader.h approach/AirfieldTargetSelectorList.cpp approach/AirfieldTargetSelectorList.h approach/ApproachSequencerDistanceOptions.cpp approach/ApproachSequencerDistanceOptions.h approach/RemoveLandedAircraft.cpp approach/RemoveLandedAircraft.h approach/ApproachFlightplanEventHandler.cpp approach/ApproachFlightplanEventHandler.h) + approach/SequencerAirfieldSelector.cpp approach/SequencerAirfieldSelector.h approach/AircraftSelectionProvider.cpp approach/AircraftSelectionProvider.h approach/TargetSelectorList.cpp approach/TargetSelectorList.h approach/ApproachSpacingCalculator.cpp approach/ApproachSpacingCalculator.h approach/ApproachSequencerOptions.cpp approach/ApproachSequencerOptions.h approach/AirfieldApproachOptions.h approach/ApproachSequencerOptionsLoader.cpp approach/ApproachSequencerOptionsLoader.h approach/AirfieldTargetSelectorList.cpp approach/AirfieldTargetSelectorList.h approach/ApproachSequencerDistanceOptions.cpp approach/ApproachSequencerDistanceOptions.h approach/RemoveLandedAircraft.cpp approach/RemoveLandedAircraft.h approach/ApproachFlightplanEventHandler.cpp approach/ApproachFlightplanEventHandler.h + approach/GlideslopeDeviationEstimator.h + approach/GlideslopeDeviationEstimator.cpp + approach/GlideslopeDeviationTagItem.cpp + approach/GlideslopeDeviationTagItem.h) source_group("src\\approach" FILES ${src__approach}) set(src__bootstrap @@ -282,7 +286,11 @@ set(src__flightrule source_group("src\\flightrule" FILES ${src__flightrule}) set(src__geometry - geometry/Line.cpp geometry/Line.h geometry/DistanceRadiusToScreenRadius.cpp geometry/DistanceRadiusToScreenRadius.h geometry/MeasurementUnitType.h geometry/MeasurementUnitFactory.cpp geometry/MeasurementUnitFactory.h geometry/Measurement.cpp geometry/Measurement.h geometry/MeasurementUnit.cpp geometry/MeasurementUnit.h) + geometry/Line.cpp geometry/Line.h geometry/DistanceRadiusToScreenRadius.cpp geometry/DistanceRadiusToScreenRadius.h geometry/MeasurementUnitType.h geometry/MeasurementUnitFactory.cpp geometry/MeasurementUnitFactory.h geometry/Measurement.cpp geometry/Measurement.h geometry/MeasurementUnit.cpp geometry/MeasurementUnit.h + geometry/Angle.cpp + geometry/Angle.h + geometry/Length.h +) source_group("src\\geometry" FILES ${src__geometry}) set(src__graphics diff --git a/src/plugin/approach/ApproachBootstrapProvider.cpp b/src/plugin/approach/ApproachBootstrapProvider.cpp index 464691541..ac4bdadbc 100644 --- a/src/plugin/approach/ApproachBootstrapProvider.cpp +++ b/src/plugin/approach/ApproachBootstrapProvider.cpp @@ -9,6 +9,8 @@ #include "ApproachSequencerDisplayOptions.h" #include "ApproachSequencerOptionsLoader.h" #include "ApproachSpacingRingRenderer.h" +#include "GlideslopeDeviationEstimator.h" +#include "GlideslopeDeviationTagItem.h" #include "RemoveLandedAircraft.h" #include "SequencerAirfieldSelector.h" #include "TargetSelectorList.h" @@ -22,6 +24,7 @@ #include "list/PopupListFactory.h" #include "radarscreen/MenuToggleableDisplayFactory.h" #include "radarscreen/RadarRenderableCollection.h" +#include "tag/TagItemCollection.h" #include "timedevent/TimedEventCollection.h" namespace UKControllerPlugin::Approach { @@ -38,6 +41,13 @@ namespace UKControllerPlugin::Approach { container.flightplanHandler->RegisterHandler( std::make_shared(container.moduleFactories->Approach().Sequencer())); + + // Add the deviation tag item + const auto deviationEstimator = std::make_shared(); + + container.tagHandler->RegisterTagItem( + GLIDESLOPE_DEVIATION_TAG_ITEM_ID, + std::make_shared(deviationEstimator, container.runwayCollection)); } void ApproachBootstrapProvider::BootstrapRadarScreen( diff --git a/src/plugin/approach/ApproachBootstrapProvider.h b/src/plugin/approach/ApproachBootstrapProvider.h index 2fa317965..bbb8c1a68 100644 --- a/src/plugin/approach/ApproachBootstrapProvider.h +++ b/src/plugin/approach/ApproachBootstrapProvider.h @@ -14,5 +14,8 @@ namespace UKControllerPlugin::Approach { RadarScreen::ConfigurableDisplayCollection& configurables, Euroscope::AsrEventHandlerCollection& asrHandlers, const RadarScreen::MenuToggleableDisplayFactory& toggleableDisplayFactory) override; + + private: + const int GLIDESLOPE_DEVIATION_TAG_ITEM_ID = 132; }; } // namespace UKControllerPlugin::Approach diff --git a/src/plugin/approach/GlideslopeDeviationEstimator.cpp b/src/plugin/approach/GlideslopeDeviationEstimator.cpp new file mode 100644 index 000000000..fe4bacf52 --- /dev/null +++ b/src/plugin/approach/GlideslopeDeviationEstimator.cpp @@ -0,0 +1,30 @@ +#include "GlideslopeDeviationEstimator.h" +#include "euroscope/EuroScopeCRadarTargetInterface.h" +#include "runway/Runway.h" + +namespace UKControllerPlugin::Approach { + + auto GlideslopeDeviationEstimator::CalculateGlideslopeDeviation( + const Euroscope::EuroScopeCRadarTargetInterface& radarTarget, const Runway::Runway& runway) const + -> GlideslopeDeviation + { + // Calculate the slope of each line + const auto runwaySlope = runway.RunwayHeadingLineSlope(); + const auto runwayPerpendicularSlope = runway.RunwayPerpendicularHeadingLineSlope(); + + // Calculate the distance between the intersection and the threshold + EuroScopePlugIn::CPosition intersection; + intersection.m_Latitude = (runwaySlope * runway.Threshold().m_Latitude - + runwayPerpendicularSlope * radarTarget.GetPosition().m_Latitude + + radarTarget.GetPosition().m_Longitude - runway.Threshold().m_Longitude) / + (runwaySlope - runwayPerpendicularSlope); + intersection.m_Longitude = + runwaySlope * (intersection.m_Latitude - runway.Threshold().m_Latitude) + runway.Threshold().m_Longitude; + const auto distance = runway.Threshold().DistanceTo(intersection); + + return { + .deviation = radarTarget.GetAltitude() - runway.GlideslopeAltitudeAtDistance(distance), + .perpendicularDistanceFromLocaliser = radarTarget.GetPosition().DistanceTo(intersection), + .localiserRange = distance}; + } +} // namespace UKControllerPlugin::Approach diff --git a/src/plugin/approach/GlideslopeDeviationEstimator.h b/src/plugin/approach/GlideslopeDeviationEstimator.h new file mode 100644 index 000000000..c79b8231f --- /dev/null +++ b/src/plugin/approach/GlideslopeDeviationEstimator.h @@ -0,0 +1,28 @@ +#pragma once + +namespace UKControllerPlugin { + namespace Euroscope { + class EuroScopeCRadarTargetInterface; + } // namespace Euroscope + namespace Runway { + class Runway; + } // namespace Runway +} // namespace UKControllerPlugin + +namespace UKControllerPlugin::Approach { + // Struct representing glideslope deviation + struct GlideslopeDeviation + { + int deviation; + double perpendicularDistanceFromLocaliser; + double localiserRange; + }; + + class GlideslopeDeviationEstimator + { + public: + [nodiscard] auto CalculateGlideslopeDeviation( + const Euroscope::EuroScopeCRadarTargetInterface& radarTarget, + const Runway::Runway& runway) const -> GlideslopeDeviation; + }; +} // namespace UKControllerPlugin::Approach diff --git a/src/plugin/approach/GlideslopeDeviationTagItem.cpp b/src/plugin/approach/GlideslopeDeviationTagItem.cpp new file mode 100644 index 000000000..4b665f662 --- /dev/null +++ b/src/plugin/approach/GlideslopeDeviationTagItem.cpp @@ -0,0 +1,75 @@ +#include "GlideslopeDeviationEstimator.h" +#include "GlideslopeDeviationTagItem.h" +#include "euroscope/EuroScopeCFlightPlanInterface.h" +#include "euroscope/EuroScopeCRadarTargetInterface.h" +#include "runway/Runway.h" +#include "runway/RunwayCollection.h" +#include "tag/TagData.h" + +namespace UKControllerPlugin::Approach { + GlideslopeDeviationTagItem::GlideslopeDeviationTagItem( + std::shared_ptr glideslopeDeviationEstimator, + std::shared_ptr runways) + : glideslopeDeviationEstimator(glideslopeDeviationEstimator), runways(runways) + { + assert(this->glideslopeDeviationEstimator != nullptr && "Glideslope deviation estimator cannot be null"); + assert(this->runways != nullptr && "Runways cannot be null"); + } + + std::string GlideslopeDeviationTagItem::GetTagItemDescription(int tagItemId) const + { + switch (tagItemId) { + case 132: + return "Glideslope Deviation"; + default: + throw std::invalid_argument("Invalid tag item ID"); + } + } + + void GlideslopeDeviationTagItem::SetTagItemData(Tag::TagData& tagData) + { + const auto& flightplan = tagData.GetFlightplan(); + + // Get the runway + const auto runway = + runways->GetByAirfieldAndIdentifier(flightplan.GetDestination(), flightplan.GetArrivalRunway()); + if (runway == nullptr) { + return; + } + + // Make sure we're upwind of the runway + if (std::abs(tagData.GetRadarTarget().GetPosition().DirectionTo(runway->Threshold()) - runway->Heading()) > + 90) { + return; + } + + // Calculate the deviation and make sure we're somewhat close + const auto deviation = + glideslopeDeviationEstimator->CalculateGlideslopeDeviation(tagData.GetRadarTarget(), *runway); + if (deviation.perpendicularDistanceFromLocaliser > 15) { + return; + } + + if (deviation.localiserRange > 25) { + return; + } + + // Set the tag colour to red if we're massively out + if (deviation.deviation > 300) { + tagData.SetTagColour(RGB(255, 87, 51)); + } + + // If we're massively out, abbreviate the string + if (deviation.deviation > 999) { + tagData.SetItemString(">1k"); + return; + } else if (deviation.deviation < -999) { + tagData.SetItemString("<1k"); + return; + } + + // Set the tag item string + const auto deviationSign = deviation.deviation >= 0 ? "+" : ""; + tagData.SetItemString(deviationSign + std::to_string(deviation.deviation)); + } +} // namespace UKControllerPlugin::Approach diff --git a/src/plugin/approach/GlideslopeDeviationTagItem.h b/src/plugin/approach/GlideslopeDeviationTagItem.h new file mode 100644 index 000000000..9cec006ac --- /dev/null +++ b/src/plugin/approach/GlideslopeDeviationTagItem.h @@ -0,0 +1,31 @@ +#pragma once +#include "tag/TagItemInterface.h" + +namespace UKControllerPlugin { + namespace Runway { + class RunwayCollection; + } // namespace Runway +} // namespace UKControllerPlugin + +namespace UKControllerPlugin::Approach { + + class GlideslopeDeviationEstimator; + + class GlideslopeDeviationTagItem : public Tag::TagItemInterface + { + public: + GlideslopeDeviationTagItem( + std::shared_ptr glideslopeDeviationEstimator, + std::shared_ptr runways); + auto GetTagItemDescription(int tagItemId) const -> std::string override; + void SetTagItemData(Tag::TagData& tagData) override; + + private: + // The glideslope deviation estimator + std::shared_ptr glideslopeDeviationEstimator; + + // The runways + std::shared_ptr runways; + }; + +} // namespace UKControllerPlugin::Approach diff --git a/src/plugin/bootstrap/PersistenceContainer.h b/src/plugin/bootstrap/PersistenceContainer.h index 4d5ca2a11..10e8e06f1 100644 --- a/src/plugin/bootstrap/PersistenceContainer.h +++ b/src/plugin/bootstrap/PersistenceContainer.h @@ -260,7 +260,7 @@ namespace UKControllerPlugin::Bootstrap { std::unique_ptr wakeCategoryMappers; std::shared_ptr publishedHolds; std::unique_ptr flightRules; - std::unique_ptr runwayCollection; + std::shared_ptr runwayCollection; // Push events std::shared_ptr pushEventProcessors; diff --git a/src/plugin/euroscope/EuroScopeCFlightPlanInterface.h b/src/plugin/euroscope/EuroScopeCFlightPlanInterface.h index 1f4a6d8c7..95613b03c 100644 --- a/src/plugin/euroscope/EuroScopeCFlightPlanInterface.h +++ b/src/plugin/euroscope/EuroScopeCFlightPlanInterface.h @@ -51,5 +51,6 @@ namespace UKControllerPlugin::Euroscope { [[nodiscard]] virtual EuroScopePlugIn::CFlightPlan& GetEuroScopeObject() const = 0; [[nodiscard]] virtual auto GetRemarks() const -> std::string = 0; [[nodiscard]] virtual auto GetDepartureRunway() const -> std::string = 0; + [[nodiscard]] virtual auto GetArrivalRunway() const -> std::string = 0; }; } // namespace UKControllerPlugin::Euroscope diff --git a/src/plugin/euroscope/EuroScopeCFlightPlanWrapper.cpp b/src/plugin/euroscope/EuroScopeCFlightPlanWrapper.cpp index f4ea05fcd..a5cf4219f 100644 --- a/src/plugin/euroscope/EuroScopeCFlightPlanWrapper.cpp +++ b/src/plugin/euroscope/EuroScopeCFlightPlanWrapper.cpp @@ -220,4 +220,9 @@ namespace UKControllerPlugin::Euroscope { { return this->originalData.GetFlightPlanData().GetDepartureRwy(); } + + std::string EuroScopeCFlightPlanWrapper::GetArrivalRunway() const + { + return this->originalData.GetFlightPlanData().GetArrivalRwy(); + } } // namespace UKControllerPlugin::Euroscope diff --git a/src/plugin/euroscope/EuroScopeCFlightPlanWrapper.h b/src/plugin/euroscope/EuroScopeCFlightPlanWrapper.h index da478bc1b..f5d3a7e28 100644 --- a/src/plugin/euroscope/EuroScopeCFlightPlanWrapper.h +++ b/src/plugin/euroscope/EuroScopeCFlightPlanWrapper.h @@ -32,6 +32,7 @@ namespace UKControllerPlugin::Euroscope { std::string GetRawRouteString() const override; std::string GetSidName() const override; std::string GetDepartureRunway() const override; + std::string GetArrivalRunway() const override; bool HasAssignedSquawk() const override; bool HasControllerClearedAltitude() const override; bool HasControllerAssignedHeading() const override; diff --git a/src/plugin/geometry/Angle.cpp b/src/plugin/geometry/Angle.cpp new file mode 100644 index 000000000..0c09e3ba6 --- /dev/null +++ b/src/plugin/geometry/Angle.cpp @@ -0,0 +1,21 @@ +#include "Angle.h" + +namespace UKControllerPlugin::Geometry { + + const double pi = 3.14159265358979323846; + + auto DegreesToRadians(const double degrees) -> double + { + return degrees * pi / 180; + } + + auto RadiansToDegrees(const double radians) -> double + { + return radians * 180 / pi; + } + + auto Slope(const double radians) -> double + { + return std::tan(radians); + } +} // namespace UKControllerPlugin::Geometry diff --git a/src/plugin/geometry/Angle.h b/src/plugin/geometry/Angle.h new file mode 100644 index 000000000..8e9bc4184 --- /dev/null +++ b/src/plugin/geometry/Angle.h @@ -0,0 +1,8 @@ +#pragma once + +namespace UKControllerPlugin::Geometry { + [[nodiscard]] auto DegreesToRadians(const double degrees) -> double; + [[nodiscard]] auto RadiansToDegrees(const double radians) -> double; + [[nodiscard]] auto Slope(const double radians) -> double; + +} // namespace UKControllerPlugin::Geometry diff --git a/src/plugin/geometry/Length.h b/src/plugin/geometry/Length.h new file mode 100644 index 000000000..26e672a9b --- /dev/null +++ b/src/plugin/geometry/Length.h @@ -0,0 +1,13 @@ +#pragma once + +namespace UKControllerPlugin::Geometry { + [[nodiscard]] inline auto NauticalMilesToFeet(double nauticalMiles) -> double + { + return nauticalMiles * 6076.115; + } + + [[nodiscard]] inline auto FeetToNauticalMiles(double feet) -> double + { + return feet / 6076.115; + } +} // namespace UKControllerPlugin::Geometry diff --git a/src/plugin/headings/Heading.cpp b/src/plugin/headings/Heading.cpp index c9af4f3db..c978ca13f 100644 --- a/src/plugin/headings/Heading.cpp +++ b/src/plugin/headings/Heading.cpp @@ -25,4 +25,14 @@ namespace UKControllerPlugin::Headings { { return first == static_cast(second); } + + [[nodiscard]] auto TruncateHeading(unsigned int heading) -> unsigned int + { + return heading % 360; + } + + [[nodiscard]] auto PerpendicularHeading(unsigned int heading) -> unsigned int + { + return TruncateHeading(heading + 90); + } } // namespace UKControllerPlugin::Headings diff --git a/src/plugin/headings/Heading.h b/src/plugin/headings/Heading.h index fb771110e..12771532e 100644 --- a/src/plugin/headings/Heading.h +++ b/src/plugin/headings/Heading.h @@ -26,4 +26,7 @@ namespace UKControllerPlugin::Headings { auto operator<(double first, Heading second) -> bool; auto operator>=(double first, Heading second) -> bool; auto operator==(unsigned int first, Heading second) -> bool; + + [[nodiscard]] auto TruncateHeading(unsigned int heading) -> unsigned int; + [[nodiscard]] auto PerpendicularHeading(unsigned int heading) -> unsigned int; } // namespace UKControllerPlugin::Headings diff --git a/src/plugin/runway/Runway.cpp b/src/plugin/runway/Runway.cpp index b27355a5a..c728043de 100644 --- a/src/plugin/runway/Runway.cpp +++ b/src/plugin/runway/Runway.cpp @@ -1,9 +1,27 @@ #include "Runway.h" +#include "geometry/Angle.h" +#include "geometry/Length.h" +#include "headings/Heading.h" namespace UKControllerPlugin::Runway { - Runway::Runway(int id, int airfieldId, std::string identifier, int heading, EuroScopePlugIn::CPosition threshold) - : id(id), airfieldId(airfieldId), identifier(std::move(identifier)), heading(heading), threshold(threshold) + Runway::Runway( + int id, + int airfieldId, + std::string airfieldIdentifier, + std::string identifier, + int heading, + EuroScopePlugIn::CPosition threshold, + int thresholdElevation, + double glideslopeAngle) + : id(id), airfieldId(airfieldId), airfieldIdentifier(airfieldIdentifier), identifier(std::move(identifier)), + heading(heading), headingRadians(Geometry::DegreesToRadians(heading)), + perpendicularHeading(Headings::PerpendicularHeading(heading)), + perpendicularHeadingRadians(Geometry::DegreesToRadians(perpendicularHeading)), + runwayHeadingLineSlope(Geometry::Slope(headingRadians)), + runwayPerpendicularHeadingLineSlope(Geometry::Slope(perpendicularHeadingRadians)), threshold(threshold), + thresholdElevation(thresholdElevation), glideslopeAngle(glideslopeAngle), + glideslopeAngleRadians(Geometry::DegreesToRadians(glideslopeAngle)) { } @@ -17,6 +35,11 @@ namespace UKControllerPlugin::Runway { return airfieldId; } + auto Runway::AirfieldIdentifier() const -> const std::string& + { + return airfieldIdentifier; + } + auto Runway::Identifier() const -> const std::string& { return identifier; @@ -31,4 +54,29 @@ namespace UKControllerPlugin::Runway { { return threshold; } + + auto Runway::GlideslopeAltitudeAtDistance(const double distanceInNauticalMiles) const -> int + { + return (glideslopeAngleRadians * Geometry::NauticalMilesToFeet(distanceInNauticalMiles)) + thresholdElevation; + } + + auto Runway::RunwayHeadingLineSlope() const -> double + { + return runwayHeadingLineSlope; + } + + auto Runway::RunwayPerpendicularHeadingLineSlope() const -> double + { + return runwayPerpendicularHeadingLineSlope; + } + + auto Runway::ThresholdElevation() const -> int + { + return thresholdElevation; + } + + auto Runway::GlideslopeAngle() const -> double + { + return glideslopeAngle; + } } // namespace UKControllerPlugin::Runway diff --git a/src/plugin/runway/Runway.h b/src/plugin/runway/Runway.h index 59f682b56..d33a81732 100644 --- a/src/plugin/runway/Runway.h +++ b/src/plugin/runway/Runway.h @@ -7,27 +7,71 @@ namespace UKControllerPlugin::Runway { class Runway { public: - Runway(int id, int airfieldId, std::string identifier, int heading, EuroScopePlugIn::CPosition threshold); + Runway( + int id, + int airfieldId, + std::string airfieldIdentifier, + std::string identifier, + int heading, + EuroScopePlugIn::CPosition threshold, + int thresholdElevation, + double glideslopeAngle); [[nodiscard]] auto Id() const -> int; [[nodiscard]] auto AirfieldId() const -> int; + [[nodiscard]] auto AirfieldIdentifier() const -> const std::string&; [[nodiscard]] auto Identifier() const -> const std::string&; [[nodiscard]] auto Heading() const -> int; [[nodiscard]] auto Threshold() const -> const EuroScopePlugIn::CPosition&; + [[nodiscard]] auto ThresholdElevation() const -> int; + [[nodiscard]] auto GlideslopeAngle() const -> double; + [[nodiscard]] auto GlideslopeAltitudeAtDistance(const double distanceInNauticalMiles) const -> int; + [[nodiscard]] auto RunwayHeadingLineSlope() const -> double; + [[nodiscard]] auto RunwayPerpendicularHeadingLineSlope() const -> double; private: + // A convenient calculation of pi + const double pi = std::atan(1) * 4; + // The id of the runway in the API int id; // The id of the airfield that this runway is at int airfieldId; + // The identifier of the airfield that this runway is at + std::string airfieldIdentifier; + // The identifier of the runway - e.g 27L std::string identifier; // The heading of the runway int heading; + // The heading of the runway in radians + double headingRadians; + + // The perpendicular heading of the runway + int perpendicularHeading; + + // The perpendicular heading of the runway in radians + double perpendicularHeadingRadians; + + // The slope of the runway heading line + double runwayHeadingLineSlope; + + // The slope of the runway perpendicular heading line + double runwayPerpendicularHeadingLineSlope; + // The coordinates of the runway threshold EuroScopePlugIn::CPosition threshold; + + // The elevation of the runway threshold + int thresholdElevation; + + // The glideslope angle in degrees + double glideslopeAngle; + + // The glideslope angle (in radians) + double glideslopeAngleRadians; }; } // namespace UKControllerPlugin::Runway diff --git a/src/plugin/runway/RunwayCollection.cpp b/src/plugin/runway/RunwayCollection.cpp index 87ad6b095..ffdffe606 100644 --- a/src/plugin/runway/RunwayCollection.cpp +++ b/src/plugin/runway/RunwayCollection.cpp @@ -11,6 +11,8 @@ namespace UKControllerPlugin::Runway { } runways[runway->Id()] = runway; + runwaysByAirfieldIdAndIdentifier[runway->AirfieldId()][runway->Identifier()] = runway; + runwaysByAirfieldAndIdentifier[runway->AirfieldIdentifier()][runway->Identifier()] = runway; } auto RunwayCollection::Count() const -> size_t @@ -26,12 +28,28 @@ namespace UKControllerPlugin::Runway { auto RunwayCollection::GetByAirfieldAndIdentifier(int airfieldId, const std::string& identifier) const -> std::shared_ptr { - auto runway = std::find_if( - runways.begin(), - runways.end(), - [&airfieldId, &identifier](std::pair&> runway) -> bool { - return runway.second->AirfieldId() == airfieldId && runway.second->Identifier() == identifier; - }); - return runway != runways.cend() ? runway->second : nullptr; + if (runwaysByAirfieldIdAndIdentifier.count(airfieldId) == 0) { + return nullptr; + } + + if (runwaysByAirfieldIdAndIdentifier.at(airfieldId).count(identifier) == 0) { + return nullptr; + } + + return runwaysByAirfieldIdAndIdentifier.at(airfieldId).at(identifier); + } + + auto RunwayCollection::GetByAirfieldAndIdentifier(const std::string& airfield, const std::string& identifier) const + -> std::shared_ptr + { + if (runwaysByAirfieldAndIdentifier.count(airfield) == 0) { + return nullptr; + } + + if (runwaysByAirfieldAndIdentifier.at(airfield).count(identifier) == 0) { + return nullptr; + } + + return runwaysByAirfieldAndIdentifier.at(airfield).at(identifier); } } // namespace UKControllerPlugin::Runway diff --git a/src/plugin/runway/RunwayCollection.h b/src/plugin/runway/RunwayCollection.h index bded5309e..55cfdec82 100644 --- a/src/plugin/runway/RunwayCollection.h +++ b/src/plugin/runway/RunwayCollection.h @@ -1,5 +1,4 @@ #pragma once - #include "RunwayCollectionFactory.h" namespace UKControllerPlugin::Runway { @@ -16,9 +15,15 @@ namespace UKControllerPlugin::Runway { [[nodiscard]] auto GetById(int id) const -> std::shared_ptr; [[nodiscard]] auto GetByAirfieldAndIdentifier(int airfieldId, const std::string& identifier) const -> std::shared_ptr; + [[nodiscard]] auto GetByAirfieldAndIdentifier(const std::string& airfield, const std::string& identifier) const + -> std::shared_ptr; private: // All the runways std::map> runways; + std::unordered_map>> + runwaysByAirfieldIdAndIdentifier; + std::unordered_map>> + runwaysByAirfieldAndIdentifier; }; } // namespace UKControllerPlugin::Runway diff --git a/src/plugin/runway/RunwayCollectionFactory.cpp b/src/plugin/runway/RunwayCollectionFactory.cpp index 4f61df0d9..17e945340 100644 --- a/src/plugin/runway/RunwayCollectionFactory.cpp +++ b/src/plugin/runway/RunwayCollectionFactory.cpp @@ -1,18 +1,21 @@ #include "Runway.h" #include "RunwayCollection.h" #include "RunwayCollectionFactory.h" +#include "airfield/AirfieldModel.h" +#include "airfield/AirfieldCollection.h" namespace UKControllerPlugin::Runway { - auto BuildRunwayCollection(const nlohmann::json& dependency) -> std::unique_ptr + auto BuildRunwayCollection(const nlohmann::json& dependency, const Airfield::AirfieldCollection& airfields) + -> std::shared_ptr { - auto collection = std::make_unique(); + auto collection = std::make_shared(); if (!dependency.is_array()) { LogWarning("Runway dependency is not an array"); return collection; } for (const auto& runway : dependency) { - if (!RunwayValid(runway)) { + if (!RunwayValid(runway, airfields)) { LogWarning("Invalid runway detected"); continue; } @@ -24,22 +27,28 @@ namespace UKControllerPlugin::Runway { collection->Add(std::make_shared( runway.at("id").get(), runway.at("airfield_id").get(), + airfields.FetchById(runway.at("airfield_id").get())->Icao(), runway.at("identifier").get(), runway.at("heading").get(), - std::move(threshold))); + std::move(threshold), + runway.at("threshold_elevation").get(), + runway.at("glideslope_angle").get())); } LogInfo("Loaded " + std::to_string(collection->Count()) + " runways"); return collection; } - auto RunwayValid(const nlohmann::json& runway) -> bool + auto RunwayValid(const nlohmann::json& runway, const Airfield::AirfieldCollection& airfields) -> bool { return runway.is_object() && runway.contains("id") && runway.at("id").is_number_integer() && runway.contains("airfield_id") && runway.at("airfield_id").is_number_integer() && - runway.contains("identifier") && runway.at("identifier").is_string() && runway.contains("heading") && + airfields.FetchById(runway.at("airfield_id").get()) != nullptr && runway.contains("identifier") && + runway.at("identifier").is_string() && runway.contains("heading") && runway.at("heading").is_number_integer() && runway.contains("threshold_latitude") && runway.at("threshold_latitude").is_number() && runway.contains("threshold_longitude") && - runway.at("threshold_longitude").is_number(); + runway.at("threshold_longitude").is_number() && runway.contains("threshold_elevation") && + runway.at("threshold_elevation").is_number_integer() && runway.contains("glideslope_angle") && + runway.at("glideslope_angle").is_number(); } } // namespace UKControllerPlugin::Runway diff --git a/src/plugin/runway/RunwayCollectionFactory.h b/src/plugin/runway/RunwayCollectionFactory.h index 83830e171..9f15e5590 100644 --- a/src/plugin/runway/RunwayCollectionFactory.h +++ b/src/plugin/runway/RunwayCollectionFactory.h @@ -1,6 +1,9 @@ #pragma once namespace UKControllerPlugin { + namespace Airfield { + class AirfieldCollection; + } // namespace Airfield namespace Bootstrap { struct PersistenceContainer; } // namespace Bootstrap @@ -12,6 +15,7 @@ namespace UKControllerPlugin { namespace UKControllerPlugin::Runway { class RunwayCollection; - auto BuildRunwayCollection(const nlohmann::json& dependency) -> std::unique_ptr; - auto RunwayValid(const nlohmann::json& runway) -> bool; + auto BuildRunwayCollection(const nlohmann::json& dependency, const Airfield::AirfieldCollection& airfields) + -> std::shared_ptr; + auto RunwayValid(const nlohmann::json& runway, const Airfield::AirfieldCollection& airfields) -> bool; } // namespace UKControllerPlugin::Runway diff --git a/src/plugin/runway/RunwayModule.cpp b/src/plugin/runway/RunwayModule.cpp index 3d1da1ef3..c058b9699 100644 --- a/src/plugin/runway/RunwayModule.cpp +++ b/src/plugin/runway/RunwayModule.cpp @@ -9,7 +9,7 @@ namespace UKControllerPlugin::Runway { void BootstrapPlugin(Bootstrap::PersistenceContainer& container, Dependency::DependencyLoaderInterface& dependencyLoader) { - container.runwayCollection = - BuildRunwayCollection(dependencyLoader.LoadDependency("DEPENDENCY_RUNWAYS", nlohmann::json::array())); + container.runwayCollection = BuildRunwayCollection( + dependencyLoader.LoadDependency("DEPENDENCY_RUNWAYS", nlohmann::json::array()), *container.airfields); } } // namespace UKControllerPlugin::Runway diff --git a/test/plugin/CMakeLists.txt b/test/plugin/CMakeLists.txt index ffa967be9..78d770a3e 100644 --- a/test/plugin/CMakeLists.txt +++ b/test/plugin/CMakeLists.txt @@ -36,7 +36,9 @@ set(test__approach approach/ApproachSequencerDisplayOptionsTest.cpp approach/ApproachSequencerDisplayAsrLoaderTest.cpp approach/ToggleApproachSequencerDisplayTest.cpp - approach/SequencerAirfieldSelectorTest.cpp approach/ApproachModuleFactoryTest.cpp approach/AircraftSelectionProviderTest.cpp approach/TargetSelectorListTest.cpp wake/ApproachSpacingCalculatorTest.cpp approach/ApproachSequencerOptionsTest.cpp approach/ApproachSequencerOptionsLoaderTest.cpp approach/AirfieldTargetSelectorListTest.cpp approach/ApproachSequencerDistanceOptionsTest.cpp approach/AirfieldMinimumSeparationSelectorListTest.cpp approach/RemoveLandedAircraftTest.cpp approach/ApproachFlightplanEventHandlerTest.cpp) + approach/SequencerAirfieldSelectorTest.cpp approach/ApproachModuleFactoryTest.cpp approach/AircraftSelectionProviderTest.cpp approach/TargetSelectorListTest.cpp wake/ApproachSpacingCalculatorTest.cpp approach/ApproachSequencerOptionsTest.cpp approach/ApproachSequencerOptionsLoaderTest.cpp approach/AirfieldTargetSelectorListTest.cpp approach/ApproachSequencerDistanceOptionsTest.cpp approach/AirfieldMinimumSeparationSelectorListTest.cpp approach/RemoveLandedAircraftTest.cpp approach/ApproachFlightplanEventHandlerTest.cpp + approach/GlideslopeDeviationEstimatorTest.cpp + approach/GlideslopeDeviationTagItemTest.cpp) source_group("test\\approach" FILES ${test__approach}) set(test__bootstrap @@ -152,7 +154,9 @@ set(test__flightrule source_group("test\\flightrule" FILES ${test__flightrule}) set(test__geometry - geometry/LineTest.cpp geometry/DistanceRadiusToScreenRadiusTest.cpp geometry/MeasurementUnitFactoryTest.cpp) + geometry/LineTest.cpp geometry/DistanceRadiusToScreenRadiusTest.cpp geometry/MeasurementUnitFactoryTest.cpp + geometry/AngleTest.cpp + geometry/LengthTest.cpp) source_group("test\\geometry" FILES ${test__geometry}) set(test__handoff diff --git a/test/plugin/approach/ApproachBootstrapProviderTest.cpp b/test/plugin/approach/ApproachBootstrapProviderTest.cpp index a257fd3e9..f3b9bb141 100644 --- a/test/plugin/approach/ApproachBootstrapProviderTest.cpp +++ b/test/plugin/approach/ApproachBootstrapProviderTest.cpp @@ -3,6 +3,8 @@ #include "euroscope/PluginSettingsProviderCollection.h" #include "flightplan/FlightPlanEventHandlerCollection.h" #include "plugin/FunctionCallEventHandler.h" +#include "runway/RunwayCollection.h" +#include "tag/TagItemCollection.h" #include "timedevent/TimedEventCollection.h" #include "wake/WakeCategoryMapperCollection.h" @@ -26,6 +28,7 @@ namespace UKControllerPluginTest::Approach { container.flightplanHandler = std::make_unique(); container.pluginSettingsProviders = std::make_unique(*container.pluginUserSettingHandler); + container.runwayCollection = std::make_shared(); } ApproachBootstrapProvider provider; @@ -50,6 +53,13 @@ namespace UKControllerPluginTest::Approach { EXPECT_EQ(1, container.flightplanHandler->CountHandlers()); } + TEST_F(ApproachBootstrapProviderTest, ItRegistersTheGlideslopeDeviationTagItem) + { + this->RunBootstrapPlugin(provider); + EXPECT_EQ(1, container.tagHandler->CountHandlers()); + EXPECT_TRUE(container.tagHandler->HasHandlerForItemId(132)); + } + TEST_F(ApproachBootstrapProviderTest, ItRegistersTheRenderers) { this->RunBootstrapRadarScreen(provider); diff --git a/test/plugin/approach/GlideslopeDeviationEstimatorTest.cpp b/test/plugin/approach/GlideslopeDeviationEstimatorTest.cpp new file mode 100644 index 000000000..7f4126a7e --- /dev/null +++ b/test/plugin/approach/GlideslopeDeviationEstimatorTest.cpp @@ -0,0 +1,39 @@ +#include "approach/GlideslopeDeviationEstimator.h" +#include "runway/Runway.h" + +namespace UKControllerPluginTest::Approach { + class GlideslopeDeviationEstimatorTest : public ::testing::Test + { + public: + GlideslopeDeviationEstimatorTest() : runway(1, 1, "EGKK", "26L", 257, RunwayPosition(), 196, 3.0) + { + } + + [[nodiscard]] auto RunwayPosition() -> EuroScopePlugIn::CPosition + { + EuroScopePlugIn::CPosition pos; + pos.m_Latitude = 51.150675; + pos.m_Longitude = -0.171925; + return pos; + } + + UKControllerPlugin::Runway::Runway runway; + testing::NiceMock radarTarget; + UKControllerPlugin::Approach::GlideslopeDeviationEstimator glideslopeDeviationEstimator; + }; + + TEST_F(GlideslopeDeviationEstimatorTest, CalculateGlideslopeDeviation) + { + EuroScopePlugIn::CPosition aircraftPosition; + // Approx 8DME + aircraftPosition.m_Latitude = 51.17575; + aircraftPosition.m_Longitude = 0.00829; + ON_CALL(radarTarget, GetPosition()).WillByDefault(testing::Return(aircraftPosition)); + ON_CALL(radarTarget, GetAltitude()).WillByDefault(testing::Return(2000)); + + const auto result = glideslopeDeviationEstimator.CalculateGlideslopeDeviation(radarTarget, runway); + EXPECT_DOUBLE_EQ(-448, result.deviation); + EXPECT_NEAR(0.9515431, result.perpendicularDistanceFromLocaliser, 0.001); + EXPECT_NEAR(7.0799, result.localiserRange, 0.001); + } +} // namespace UKControllerPluginTest::Approach diff --git a/test/plugin/approach/GlideslopeDeviationTagItemTest.cpp b/test/plugin/approach/GlideslopeDeviationTagItemTest.cpp new file mode 100644 index 000000000..241a34180 --- /dev/null +++ b/test/plugin/approach/GlideslopeDeviationTagItemTest.cpp @@ -0,0 +1,175 @@ +#include "approach/GlideslopeDeviationEstimator.h" +#include "approach/GlideslopeDeviationTagItem.h" +#include "runway/Runway.h" +#include "runway/RunwayCollection.h" +#include "tag/TagData.h" + +namespace UKControllerPluginTest::Approach { + class GlideslopeDeviationTagItemTest : public testing::Test + { + public: + GlideslopeDeviationTagItemTest() + : tagData( + mockFlightplan, + mockRadarTarget, + 132, + EuroScopePlugIn::TAG_DATA_CORRELATED, + itemString, + &euroscopeColourCode, + &tagColour, + &fontSize), + runways(std::make_shared()), + glideslopeDeviationEstimator( + std::make_shared()), + glideslopeDeviationTagItem(glideslopeDeviationEstimator, runways) + { + // Add a runway to the collection, based on Gatwick's 26L + EuroScopePlugIn::CPosition pos; + pos.m_Latitude = 51.150675; + pos.m_Longitude = -0.171925; + runways->Add(std::make_shared(1, 1, "EGKK", "26L", 257, pos, 196, 3.0)); + + // Flightplan + ON_CALL(mockFlightplan, GetCallsign()).WillByDefault(testing::Return("BAW123")); + ON_CALL(mockFlightplan, GetDestination()).WillByDefault(testing::Return("EGKK")); + ON_CALL(mockFlightplan, GetArrivalRunway()).WillByDefault(testing::Return("26L")); + + // Aircraft position + EuroScopePlugIn::CPosition aircraftPosition; + aircraftPosition.m_Latitude = 51.17575; + aircraftPosition.m_Longitude = 0.00829; + ON_CALL(mockRadarTarget, GetPosition()).WillByDefault(testing::Return(aircraftPosition)); + } + + double fontSize = 24.1; + COLORREF tagColour = RGB(255, 255, 255); + int euroscopeColourCode = EuroScopePlugIn::TAG_COLOR_ASSUMED; + char itemString[16] = "Foooooo"; + testing::NiceMock mockFlightplan; + testing::NiceMock mockRadarTarget; + UKControllerPlugin::Tag::TagData tagData; + + std::shared_ptr runways; + std::shared_ptr glideslopeDeviationEstimator; + UKControllerPlugin::Approach::GlideslopeDeviationTagItem glideslopeDeviationTagItem; + }; + + TEST_F(GlideslopeDeviationTagItemTest, ItHasATagItemDescription) + { + EXPECT_EQ("Glideslope Deviation", glideslopeDeviationTagItem.GetTagItemDescription(132)); + } + + TEST_F(GlideslopeDeviationTagItemTest, ItThrowsExceptionIfAskedAboutInvalidTagItem) + { + EXPECT_THROW(glideslopeDeviationTagItem.GetTagItemDescription(0), std::invalid_argument); + } + + TEST_F(GlideslopeDeviationTagItemTest, ItSetsTagItemDataWellAboveGlideslope) + { + ON_CALL(mockRadarTarget, GetAltitude()).WillByDefault(testing::Return(2896)); + + glideslopeDeviationTagItem.SetTagItemData(tagData); + + EXPECT_EQ("+448", tagData.GetItemString()); + EXPECT_EQ(RGB(255, 87, 51), tagData.GetTagColour()); + } + + TEST_F(GlideslopeDeviationTagItemTest, ItSetsTagItemDataSlightlyAbove) + { + ON_CALL(mockRadarTarget, GetAltitude()).WillByDefault(testing::Return(2496)); + + glideslopeDeviationTagItem.SetTagItemData(tagData); + + EXPECT_EQ("+48", tagData.GetItemString()); + EXPECT_EQ(RGB(255, 255, 255), tagData.GetTagColour()); + } + + TEST_F(GlideslopeDeviationTagItemTest, ItSetsTagItemDataWellBelowGlideslope) + { + ON_CALL(mockRadarTarget, GetAltitude()).WillByDefault(testing::Return(2000)); + + glideslopeDeviationTagItem.SetTagItemData(tagData); + + EXPECT_EQ("-448", tagData.GetItemString()); + EXPECT_EQ(RGB(255, 255, 255), tagData.GetTagColour()); + } + + TEST_F(GlideslopeDeviationTagItemTest, ItSetsTagItemDataSlightlyBelow) + { + ON_CALL(mockRadarTarget, GetAltitude()).WillByDefault(testing::Return(2400)); + + glideslopeDeviationTagItem.SetTagItemData(tagData); + + EXPECT_EQ("-48", tagData.GetItemString()); + EXPECT_EQ(RGB(255, 255, 255), tagData.GetTagColour()); + } + + TEST_F(GlideslopeDeviationTagItemTest, ItSetsTagItemDataOver1000FeetAboveGlideslope) + { + ON_CALL(mockRadarTarget, GetAltitude()).WillByDefault(testing::Return(5000)); + + glideslopeDeviationTagItem.SetTagItemData(tagData); + + EXPECT_EQ(">1k", tagData.GetItemString()); + EXPECT_EQ(RGB(255, 87, 51), tagData.GetTagColour()); + } + + TEST_F(GlideslopeDeviationTagItemTest, ItSetsTagItemDataOver1000FeetBelowGlideslope) + { + ON_CALL(mockRadarTarget, GetAltitude()).WillByDefault(testing::Return(500)); + + glideslopeDeviationTagItem.SetTagItemData(tagData); + + EXPECT_EQ("<1k", tagData.GetItemString()); + EXPECT_EQ(RGB(255, 255, 255), tagData.GetTagColour()); + } + + TEST_F(GlideslopeDeviationTagItemTest, ItDoesntSetTagItemDataIfRunwayNotFound) + { + ON_CALL(mockFlightplan, GetDestination()).WillByDefault(testing::Return("EGSS")); + ON_CALL(mockRadarTarget, GetAltitude()).WillByDefault(testing::Return(2000)); + + glideslopeDeviationTagItem.SetTagItemData(tagData); + + EXPECT_EQ("Foooooo", tagData.GetItemString()); + } + + TEST_F(GlideslopeDeviationTagItemTest, ItDoesntSetTagItemDataIfUpwindOfRunway) + { + EuroScopePlugIn::CPosition aircraftPosition; + aircraftPosition.m_Latitude = 51.06744; + aircraftPosition.m_Longitude = -0.1977; + ON_CALL(mockRadarTarget, GetPosition()).WillByDefault(testing::Return(aircraftPosition)); + ON_CALL(mockRadarTarget, GetAltitude()).WillByDefault(testing::Return(2000)); + + glideslopeDeviationTagItem.SetTagItemData(tagData); + + EXPECT_EQ("Foooooo", tagData.GetItemString()); + } + + TEST_F(GlideslopeDeviationTagItemTest, ItDoesntSetTagItemDataIfMoreThan15MilesFromLocaliser) + { + EuroScopePlugIn::CPosition aircraftPosition; + aircraftPosition.m_Latitude = 51.17575; + aircraftPosition.m_Longitude = 2.00829; + ON_CALL(mockRadarTarget, GetPosition()).WillByDefault(testing::Return(aircraftPosition)); + ON_CALL(mockRadarTarget, GetAltitude()).WillByDefault(testing::Return(2000)); + + glideslopeDeviationTagItem.SetTagItemData(tagData); + + EXPECT_EQ("Foooooo", tagData.GetItemString()); + } + + TEST_F(GlideslopeDeviationTagItemTest, ItDoesntSetTagItemDataIfMoreThan25MilesOut) + { + EuroScopePlugIn::CPosition aircraftPosition; + aircraftPosition.m_Latitude = 54.22613; + aircraftPosition.m_Longitude = 0.00829; + ON_CALL(mockRadarTarget, GetPosition()).WillByDefault(testing::Return(aircraftPosition)); + ON_CALL(mockRadarTarget, GetAltitude()).WillByDefault(testing::Return(2000)); + + glideslopeDeviationTagItem.SetTagItemData(tagData); + + EXPECT_EQ("Foooooo", tagData.GetItemString()); + } +} // namespace UKControllerPluginTest::Approach diff --git a/test/plugin/geometry/AngleTest.cpp b/test/plugin/geometry/AngleTest.cpp new file mode 100644 index 000000000..e06631e0a --- /dev/null +++ b/test/plugin/geometry/AngleTest.cpp @@ -0,0 +1,32 @@ +#include "geometry/Angle.h" + +namespace UKControllerPluginTest::Geometry { + class AngleTest : public ::testing::Test + { + public: + const double pi = 3.14159265358979323846; + }; + + TEST_F(AngleTest, DegreesToRadians) + { + EXPECT_DOUBLE_EQ(UKControllerPlugin::Geometry::DegreesToRadians(0), 0); + EXPECT_DOUBLE_EQ(UKControllerPlugin::Geometry::DegreesToRadians(90), pi / 2); + EXPECT_DOUBLE_EQ(UKControllerPlugin::Geometry::DegreesToRadians(180), pi); + EXPECT_DOUBLE_EQ(UKControllerPlugin::Geometry::DegreesToRadians(270), pi * 1.5); + EXPECT_DOUBLE_EQ(UKControllerPlugin::Geometry::DegreesToRadians(360), pi * 2); + } + + TEST_F(AngleTest, RadiansToDegrees) + { + EXPECT_EQ(UKControllerPlugin::Geometry::RadiansToDegrees(0), 0); + EXPECT_EQ(UKControllerPlugin::Geometry::RadiansToDegrees(pi / 2), 90); + EXPECT_EQ(UKControllerPlugin::Geometry::RadiansToDegrees(pi), 180); + EXPECT_EQ(UKControllerPlugin::Geometry::RadiansToDegrees(pi * 1.5), 270); + EXPECT_EQ(UKControllerPlugin::Geometry::RadiansToDegrees(pi * 2), 360); + } + + TEST_F(AngleTest, Slope) + { + EXPECT_DOUBLE_EQ(UKControllerPlugin::Geometry::Slope(pi / 2), 1.633123935319537e+16); + } +} // namespace UKControllerPluginTest::Geometry diff --git a/test/plugin/geometry/LengthTest.cpp b/test/plugin/geometry/LengthTest.cpp new file mode 100644 index 000000000..c5b0b588f --- /dev/null +++ b/test/plugin/geometry/LengthTest.cpp @@ -0,0 +1,17 @@ +#include "geometry/Length.h" + +namespace UKControllerPluginTest::Approach { + class LengthTest : public ::testing::Test + { + }; + + TEST_F(LengthTest, ItConvertsNauticalMilesToFeet) + { + EXPECT_DOUBLE_EQ(6076.115, UKControllerPlugin::Geometry::NauticalMilesToFeet(1)); + } + + TEST_F(LengthTest, ItConvertsFeetToNauticalMiles) + { + EXPECT_DOUBLE_EQ(1, UKControllerPlugin::Geometry::FeetToNauticalMiles(6076.115)); + } +} // namespace UKControllerPluginTest::Approach diff --git a/test/plugin/headings/HeadingTest.cpp b/test/plugin/headings/HeadingTest.cpp index 0d55a4503..c00157e8b 100644 --- a/test/plugin/headings/HeadingTest.cpp +++ b/test/plugin/headings/HeadingTest.cpp @@ -136,4 +136,29 @@ namespace UKControllerPluginTest::Headings { { EXPECT_FALSE(225.0 >= Heading::W); } + + TEST_F(HeadingTest, ItDoesntTruncateHeadingLessThan360) + { + EXPECT_EQ(45, UKControllerPlugin::Headings::TruncateHeading(45)); + } + + TEST_F(HeadingTest, ItTruncatesHeadingGreaterThan360) + { + EXPECT_EQ(45, UKControllerPlugin::Headings::TruncateHeading(405)); + } + + TEST_F(HeadingTest, ItTruncatesHeadingEqualTo360) + { + EXPECT_EQ(0, UKControllerPlugin::Headings::TruncateHeading(360)); + } + + TEST_F(HeadingTest, ItCalculatesPerpendicularHeadingWithNoTruncate) + { + EXPECT_EQ(90, UKControllerPlugin::Headings::PerpendicularHeading(0)); + } + + TEST_F(HeadingTest, ItCalculatesPerpendicularHeadingWithTruncate) + { + EXPECT_EQ(90, UKControllerPlugin::Headings::PerpendicularHeading(360)); + } } // namespace UKControllerPluginTest::Headings diff --git a/test/plugin/mock/MockEuroScopeCFlightplanInterface.h b/test/plugin/mock/MockEuroScopeCFlightplanInterface.h index 624377e59..c5fd695e1 100644 --- a/test/plugin/mock/MockEuroScopeCFlightplanInterface.h +++ b/test/plugin/mock/MockEuroScopeCFlightplanInterface.h @@ -34,6 +34,7 @@ namespace UKControllerPluginTest { MOCK_CONST_METHOD0(GetAssignedSquawk, std::string(void)); MOCK_CONST_METHOD0(GetRemarks, std::string()); MOCK_CONST_METHOD0(GetDepartureRunway, std::string()); + MOCK_CONST_METHOD0(GetArrivalRunway, std::string()); MOCK_CONST_METHOD0(HasControllerClearedAltitude, bool(void)); MOCK_CONST_METHOD0(HasControllerAssignedHeading, bool(void)); MOCK_CONST_METHOD0(HasAssignedSquawk, bool(void)); diff --git a/test/plugin/runway/RunwayCollectionFactoryTest.cpp b/test/plugin/runway/RunwayCollectionFactoryTest.cpp index d35cc8c68..f22d9fa51 100644 --- a/test/plugin/runway/RunwayCollectionFactoryTest.cpp +++ b/test/plugin/runway/RunwayCollectionFactoryTest.cpp @@ -1,3 +1,6 @@ +#include "airfield/AirfieldCollection.h" +#include "airfield/AirfieldModel.h" +#include "controller/ControllerPositionHierarchy.h" #include "runway/Runway.h" #include "runway/RunwayCollection.h" #include "runway/RunwayCollectionFactory.h" @@ -9,6 +12,12 @@ namespace UKControllerPluginTest::Runway { class RunwayCollectionFactoryTest : public testing::Test { public: + RunwayCollectionFactoryTest() + { + airfields.AddAirfield(std::make_shared(2, "EGKK", nullptr)); + airfields.AddAirfield(std::make_shared(3, "EGLL", nullptr)); + } + static auto MakeRunway(const nlohmann::json& overridingData = nlohmann::json::object(), const std::string& keyToRemove = "") -> const nlohmann::json @@ -20,7 +29,8 @@ namespace UKControllerPluginTest::Runway { {"heading", 123}, {"threshold_latitude", 1.2}, {"threshold_longitude", 3.4}, - }; + {"threshold_elevation", 201}, + {"glideslope_angle", 3.0}}; runway.update(overridingData); if (!keyToRemove.empty()) { @@ -29,76 +39,104 @@ namespace UKControllerPluginTest::Runway { return runway; }; + + UKControllerPlugin::Airfield::AirfieldCollection airfields; }; TEST_F(RunwayCollectionFactoryTest, RunwayIsValid) { - EXPECT_TRUE(RunwayValid(MakeRunway())); + EXPECT_TRUE(RunwayValid(MakeRunway(), airfields)); } TEST_F(RunwayCollectionFactoryTest, RunwayIsValidIntegerThreshold) { - EXPECT_TRUE(RunwayValid(MakeRunway(nlohmann::json{{"threshold_latitude", 1}, {"threshold_longitude", 2}}))); + EXPECT_TRUE( + RunwayValid(MakeRunway(nlohmann::json{{"threshold_latitude", 1}, {"threshold_longitude", 2}}), airfields)); } TEST_F(RunwayCollectionFactoryTest, RunwayIsInvalidIfIdMissing) { - EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json::object(), "id"))); + EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json::object(), "id"), airfields)); } TEST_F(RunwayCollectionFactoryTest, RunwayIsInvalidIfIdNotInteger) { - EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json{{"id", "abc"}}))); + EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json{{"id", "abc"}}), airfields)); } TEST_F(RunwayCollectionFactoryTest, RunwayIsInvalidIfAirfieldIdMissing) { - EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json::object(), "airfield_id"))); + EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json::object(), "airfield_id"), airfields)); } TEST_F(RunwayCollectionFactoryTest, RunwayIsInvalidIfAirfieldIdNotInteger) { - EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json{{"airfield_id", "abc"}}))); + EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json{{"airfield_id", "abc"}}), airfields)); + } + + TEST_F(RunwayCollectionFactoryTest, RunwayIsInvalidIfAirfieldIdNotValidAirfield) + { + EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json{{"airfield_id", 55}}), airfields)); } TEST_F(RunwayCollectionFactoryTest, RunwayIsInvalidIdentifierMissing) { - EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json::object(), "identifier"))); + EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json::object(), "identifier"), airfields)); } TEST_F(RunwayCollectionFactoryTest, RunwayIsInvalidIfIdentifierNotString) { - EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json{{"identifier", 123}}))); + EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json{{"identifier", 123}}), airfields)); } TEST_F(RunwayCollectionFactoryTest, RunwayIsInvalidIfThresholdLatitudeMissing) { - EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json::object(), "threshold_latitude"))); + EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json::object(), "threshold_latitude"), airfields)); } TEST_F(RunwayCollectionFactoryTest, RunwayIsInvalidIfThresholdLatitudeNotANumber) { - EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json{{"threshold_latitude", "abc"}}))); + EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json{{"threshold_latitude", "abc"}}), airfields)); } TEST_F(RunwayCollectionFactoryTest, RunwayIsInvalidIfThresholdLongitudeMissing) { - EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json::object(), "threshold_longitude"))); + EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json::object(), "threshold_longitude"), airfields)); } TEST_F(RunwayCollectionFactoryTest, RunwayIsInvalidIfThresholdLongitudeNotANumber) { - EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json{{"threshold_longitude", "abc"}}))); + EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json{{"threshold_longitude", "abc"}}), airfields)); + } + + TEST_F(RunwayCollectionFactoryTest, RunwayIsInvalidIfThresholdElevationMissing) + { + EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json::object(), "threshold_elevation"), airfields)); + } + + TEST_F(RunwayCollectionFactoryTest, RunwayIsInvalidIfThresholdElevationNotAnInteger) + { + EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json{{"threshold_elevation", 1.5}}), airfields)); + } + + TEST_F(RunwayCollectionFactoryTest, RunwayIsInvalidIfGlideslopeAngleMissing) + { + EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json::object(), "glideslope_angle"), airfields)); + } + + TEST_F(RunwayCollectionFactoryTest, RunwayIsInvalidIfThresholdElevationNotANumber) + { + EXPECT_FALSE(RunwayValid(MakeRunway(nlohmann::json{{"glideslope_angle", "abc"}}), airfields)); } TEST_F(RunwayCollectionFactoryTest, RunwayIsInvalidIfNotAnObject) { - EXPECT_FALSE(RunwayValid(nlohmann::json::array())); + EXPECT_FALSE(RunwayValid(nlohmann::json::array(), airfields)); } TEST_F(RunwayCollectionFactoryTest, ItReturnsEmptyCollectionIfDependencyNotArray) { - EXPECT_EQ(0, BuildRunwayCollection(nlohmann::json::object())->Count()); + EXPECT_EQ(0, BuildRunwayCollection(nlohmann::json::object(), airfields)->Count()); } TEST_F(RunwayCollectionFactoryTest, ItReturnsACollection) @@ -111,7 +149,8 @@ namespace UKControllerPluginTest::Runway { {"heading", 123}, {"threshold_latitude", 1.2}, {"threshold_longitude", 3.4}, - }); + {"threshold_elevation", 201}, + {"glideslope_angle", 3.0}}); dependency.push_back(nlohmann::json{ {"id", 2}, {"airfield_id", 3}, @@ -119,26 +158,33 @@ namespace UKControllerPluginTest::Runway { {"heading", 234}, {"threshold_latitude", 3.4}, {"threshold_longitude", 4.5}, - }); + {"threshold_elevation", 202}, + {"glideslope_angle", 3.1}}); - auto collection = BuildRunwayCollection(dependency); + auto collection = BuildRunwayCollection(dependency, airfields); EXPECT_EQ(2, collection->Count()); auto runway1 = collection->GetById(1); EXPECT_EQ(1, runway1->Id()); EXPECT_EQ(2, runway1->AirfieldId()); + EXPECT_EQ("EGKK", runway1->AirfieldIdentifier()); EXPECT_EQ("27L", runway1->Identifier()); EXPECT_EQ(123, runway1->Heading()); EXPECT_FLOAT_EQ(1.2, runway1->Threshold().m_Latitude); EXPECT_FLOAT_EQ(3.4, runway1->Threshold().m_Longitude); + EXPECT_EQ(201, runway1->ThresholdElevation()); + EXPECT_FLOAT_EQ(3.0, runway1->GlideslopeAngle()); auto runway2 = collection->GetById(2); EXPECT_EQ(2, runway2->Id()); EXPECT_EQ(3, runway2->AirfieldId()); + EXPECT_EQ("EGLL", runway2->AirfieldIdentifier()); EXPECT_EQ("04", runway2->Identifier()); EXPECT_EQ(234, runway2->Heading()); EXPECT_FLOAT_EQ(3.4, runway2->Threshold().m_Latitude); EXPECT_FLOAT_EQ(4.5, runway2->Threshold().m_Longitude); + EXPECT_EQ(202, runway2->ThresholdElevation()); + EXPECT_DOUBLE_EQ(3.1, runway2->GlideslopeAngle()); } TEST_F(RunwayCollectionFactoryTest, ItIgnoresInvalidRunways) @@ -151,7 +197,8 @@ namespace UKControllerPluginTest::Runway { {"heading", 123}, {"threshold_latitude", 1.2}, {"threshold_longitude", 3.4}, - }); + {"threshold_elevation", 201}, + {"glideslope_angle", 3.0}}); dependency.push_back(nlohmann::json{ {"id", 2}, {"airfield_id", 3}, @@ -159,8 +206,9 @@ namespace UKControllerPluginTest::Runway { {"heading", 234}, {"threshold_latitude", "abc"}, // Invalid {"threshold_longitude", 4.5}, - }); + {"threshold_elevation", 201}, + {"glideslope_angle", 3.0}}); - EXPECT_EQ(0, BuildRunwayCollection(nlohmann::json::object())->Count()); + EXPECT_EQ(0, BuildRunwayCollection(nlohmann::json::object(), airfields)->Count()); } } // namespace UKControllerPluginTest::Runway diff --git a/test/plugin/runway/RunwayCollectionTest.cpp b/test/plugin/runway/RunwayCollectionTest.cpp index ebcedd9eb..b44f49f4c 100644 --- a/test/plugin/runway/RunwayCollectionTest.cpp +++ b/test/plugin/runway/RunwayCollectionTest.cpp @@ -9,9 +9,9 @@ namespace UKControllerPluginTest::Runway { { public: RunwayCollectionTest() - : runway1(std::make_shared(1, 2, "05", 50, GetPosition())), - runway2(std::make_shared(2, 2, "27L", 270, GetPosition())), - runway3(std::make_shared(3, 3, "27L", 275, GetPosition())) + : runway1(std::make_shared(1, 2, "EGKK", "05", 50, GetPosition(), 196, 3.0)), + runway2(std::make_shared(2, 2, "EGKK", "27L", 270, GetPosition(), 196, 3.0)), + runway3(std::make_shared(3, 3, "EGLL", "27L", 275, GetPosition(), 196, 3.0)) { } @@ -101,4 +101,31 @@ namespace UKControllerPluginTest::Runway { EXPECT_EQ(nullptr, collection.GetByAirfieldAndIdentifier(2, "27R")); } + + TEST_F(RunwayCollectionTest, ItFetchesARunwayByIdentifierAndAirfildIdentifier) + { + collection.Add(runway1); + collection.Add(runway2); + collection.Add(runway3); + + EXPECT_EQ(runway1, collection.GetByAirfieldAndIdentifier("EGKK", "05")); + } + + TEST_F(RunwayCollectionTest, ItReturnsNullptrIfAirfieldIdentifierDoesntMatch) + { + collection.Add(runway1); + collection.Add(runway2); + collection.Add(runway3); + + EXPECT_EQ(nullptr, collection.GetByAirfieldAndIdentifier("EGLL", "05")); + } + + TEST_F(RunwayCollectionTest, ItReturnsNullptrIfIdentifierDoesntMatchWhenUsingAirfieldIdentifier) + { + collection.Add(runway1); + collection.Add(runway2); + collection.Add(runway3); + + EXPECT_EQ(nullptr, collection.GetByAirfieldAndIdentifier("EGKK", "27R")); + } } // namespace UKControllerPluginTest::Runway diff --git a/test/plugin/runway/RunwayTest.cpp b/test/plugin/runway/RunwayTest.cpp index d2619c7b9..ba62a44d6 100644 --- a/test/plugin/runway/RunwayTest.cpp +++ b/test/plugin/runway/RunwayTest.cpp @@ -6,7 +6,7 @@ namespace UKControllerPluginTest::Runway { class RunwayTest : public testing::Test { public: - RunwayTest() : runway(1, 2, "27L", 123, GetPosition()) + RunwayTest() : runway(1, 2, "EGKK", "27L", 123, GetPosition(), 196, 3.0) { } @@ -47,4 +47,33 @@ namespace UKControllerPluginTest::Runway { EXPECT_EQ(3, runway.Threshold().m_Latitude); EXPECT_EQ(4, runway.Threshold().m_Longitude); } + + TEST_F(RunwayTest, ItCalculatesGlideslopeAltitudeAtDistance) + { + // At the threshold + EXPECT_EQ(196, runway.GlideslopeAltitudeAtDistance(0)); + + // 6.95 nm (8 miles) + EXPECT_EQ(2407, runway.GlideslopeAltitudeAtDistance(6.95)); + } + + TEST_F(RunwayTest, ItHasAThresholdElevation) + { + EXPECT_EQ(196, runway.ThresholdElevation()); + } + + TEST_F(RunwayTest, ItHasAGlideslopeAngle) + { + EXPECT_DOUBLE_EQ(3, runway.GlideslopeAngle()); + } + + TEST_F(RunwayTest, ItCalculatesRunwayHeadingLineSlope) + { + EXPECT_DOUBLE_EQ(-1.5398649638145827, runway.RunwayHeadingLineSlope()); + } + + TEST_F(RunwayTest, ItCalculatesRunwayPerpendicularHeadingLineSlope) + { + EXPECT_DOUBLE_EQ(0.64940759319751062, runway.RunwayPerpendicularHeadingLineSlope()); + } } // namespace UKControllerPluginTest::Runway diff --git a/test/plugin/sid/FlightplanSidMapperTest.cpp b/test/plugin/sid/FlightplanSidMapperTest.cpp index 2e5c10982..dd3d906cd 100644 --- a/test/plugin/sid/FlightplanSidMapperTest.cpp +++ b/test/plugin/sid/FlightplanSidMapperTest.cpp @@ -29,7 +29,7 @@ namespace UKControllerPluginTest::Sid { EuroScopePlugIn::CPosition threshold; threshold.m_Latitude = 1; threshold.m_Latitude = 2; - this->runways.Add(std::make_shared(2, 2, "27L", 123, threshold)); + this->runways.Add(std::make_shared(2, 2, "EGLL", "27L", 123, threshold, 196, 3.0)); this->sids.AddSid(std::make_shared(1, 1, "TEST1A", 1, 2, 3)); this->sids.AddSid(std::make_shared(2, 2, "TEST1B", 3, 4, 5));