/* * Copyright (C) 2019 James Turner * * This file is part of the program FlightGear. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test_gps.hxx" #include #include #include "test_suite/FGTestApi/globals.hxx" #include "test_suite/FGTestApi/NavDataCache.hxx" #include "test_suite/FGTestApi/TestPilot.hxx" #include #include #include #include #include #include #include using namespace flightgear; ///////////////////////////////////////////////////////////////////////////// class TestFPDelegate : public FlightPlan::Delegate { public: FlightPlanRef thePlan; int sequenceCount = 0; #if 0 void sequence() override { ++sequenceCount; int newIndex = thePlan->currentIndex() + 1; if (newIndex >= thePlan->numLegs()) { thePlan->finish(); return; } thePlan->setCurrentIndex(newIndex); } #endif void currentWaypointChanged() override { } }; ///////////////////////////////////////////////////////////////////////////// // Set up function for each test. void GPSTests::setUp() { FGTestApi::setUp::initTestGlobals("gps"); FGTestApi::setUp::initNavDataCache(); setupRouteManager(); } // Clean up after each test. void GPSTests::tearDown() { FGTestApi::tearDown::shutdownTestGlobals(); } void GPSTests::setPositionAndStabilise(GPS* gps, const SGGeod& g) { FGTestApi::setPosition(g); for (int i=0; i<60; ++i) { gps->update(0.015); } } GPS* GPSTests::setupStandardGPS(SGPropertyNode_ptr config, const std::string name, const int index) { SGPropertyNode_ptr configNode(config.valid() ? config : SGPropertyNode_ptr{new SGPropertyNode}); configNode->setStringValue("name", name); configNode->setIntValue("number", index); GPS* gps(new GPS(configNode)); SGPropertyNode_ptr node = globals->get_props()->getNode("instrumentation", true)->getChild(name, index, true); node->setBoolValue("serviceable", true); globals->get_props()->setDoubleValue("systems/electrical/outputs/gps", 6.0); gps->bind(); gps->init(); globals->add_subsystem("gps", gps, SGSubsystemMgr::POST_FDM); return gps; } void GPSTests::setupRouteManager() { auto rm = globals->add_new_subsystem(); rm->bind(); rm->init(); rm->postinit(); } ///////////////////////////////////////////////////////////////////////////// void GPSTests::testBasic() { auto gps = setupStandardGPS(); FGPositioned::TypeFilter f{FGPositioned::VOR}; auto bodrumVOR = fgpositioned_cast(FGPositioned::findClosestWithIdent("BDR", SGGeod::fromDeg(27.6, 37), &f)); SGGeod p1 = SGGeodesy::direct(bodrumVOR->geod(), 45.0, 5.0 * SG_NM_TO_METER); setPositionAndStabilise(gps, p1); auto gpsNode = globals->get_props()->getNode("instrumentation/gps"); CPPUNIT_ASSERT_DOUBLES_EQUAL(p1.getLongitudeDeg(), gpsNode->getDoubleValue("indicated-longitude-deg"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(p1.getLatitudeDeg(), gpsNode->getDoubleValue("indicated-latitude-deg"), 0.01); auto pilot = SGSharedPtr(new FGTestApi::TestPilot); pilot->setSpeedKts(120); pilot->setCourseTrue(225.0); FGTestApi::runForTime(30.0); CPPUNIT_ASSERT_DOUBLES_EQUAL(225, gpsNode->getDoubleValue("indicated-track-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(120, gpsNode->getDoubleValue("indicated-ground-speed-kt"), 1); // 120kts = double speedMSec = 120 * SG_KT_TO_MPS; double components = speedMSec * (1.0 / sqrt(2.0)); CPPUNIT_ASSERT_DOUBLES_EQUAL(-components, gpsNode->getDoubleValue("ew-velocity-msec"), 0.1); CPPUNIT_ASSERT_DOUBLES_EQUAL(-components, gpsNode->getDoubleValue("ns-velocity-msec"), 0.1); CPPUNIT_ASSERT_DOUBLES_EQUAL(120 * (30.0 / 3600), gpsNode->getDoubleValue("odometer"), 0.1); CPPUNIT_ASSERT_DOUBLES_EQUAL(120 * (30.0 / 3600), gpsNode->getDoubleValue("trip-odometer"), 0.1); } void GPSTests::testOBSMode() { auto gps = setupStandardGPS(); FGPositioned::TypeFilter f{FGPositioned::VOR}; auto bodrumVOR = fgpositioned_cast(FGPositioned::findClosestWithIdent("BDR", SGGeod::fromDeg(27.6, 37), &f)); SGGeod p1 = SGGeodesy::direct(bodrumVOR->geod(), 45.0, 5.0 * SG_NM_TO_METER); setPositionAndStabilise(gps, p1); auto gpsNode = globals->get_props()->getNode("instrumentation/gps"); CPPUNIT_ASSERT_DOUBLES_EQUAL(p1.getLongitudeDeg(), gpsNode->getDoubleValue("indicated-longitude-deg"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(p1.getLatitudeDeg(), gpsNode->getDoubleValue("indicated-latitude-deg"), 0.01); gpsNode->setDoubleValue("selected-course-deg", 225); // query BDR from the GPS database gpsNode->setStringValue("scratch/query", "BDR"); gpsNode->setStringValue("scratch/type", "vor"); gpsNode->setStringValue("command", "search"); CPPUNIT_ASSERT_EQUAL(true, gpsNode->getBoolValue("scratch/valid")); CPPUNIT_ASSERT_DOUBLES_EQUAL(225.0, gpsNode->getDoubleValue("scratch/true-bearing-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(5.0, gpsNode->getDoubleValue("scratch/distance-nm"), 0.1); // select OBS mode one it gpsNode->setStringValue("command", "obs"); setPositionAndStabilise(gps, p1); CPPUNIT_ASSERT_EQUAL(std::string{"obs"}, std::string{gpsNode->getStringValue("mode")}); CPPUNIT_ASSERT_DOUBLES_EQUAL(5.0, gpsNode->getDoubleValue("wp/wp[1]/distance-nm"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(225.0, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(bodrumVOR->get_lon(), gpsNode->getDoubleValue("wp/wp[1]/longitude-deg"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(bodrumVOR->get_lat(), gpsNode->getDoubleValue("wp/wp[1]/latitude-deg"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(225.0, gpsNode->getDoubleValue("desired-course-deg"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.1); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); // off axis, angular SGGeod p2 = SGGeodesy::direct(bodrumVOR->geod(), 40.0, 4.0 * SG_NM_TO_METER); setPositionAndStabilise(gps, p2); CPPUNIT_ASSERT_EQUAL(std::string{"obs"}, std::string{gpsNode->getStringValue("mode")}); CPPUNIT_ASSERT_DOUBLES_EQUAL(4.0, gpsNode->getDoubleValue("wp/wp[1]/distance-nm"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(220.0, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(225.0, gpsNode->getDoubleValue("desired-course-deg"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(-5.0, gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.1); // off axis, perpendicular SGGeod p3 = SGGeodesy::direct(p1, 135, 0.5 * SG_NM_TO_METER); setPositionAndStabilise(gps, p3); CPPUNIT_ASSERT_DOUBLES_EQUAL(225.0, gpsNode->getDoubleValue("desired-course-deg"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.5, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); } void GPSTests::testDirectTo() { auto gps = setupStandardGPS(); FGPositioned::TypeFilter f{FGPositioned::VOR}; auto bodrumVOR = fgpositioned_cast(FGPositioned::findClosestWithIdent("BDR", SGGeod::fromDeg(27.6, 37), &f)); SGGeod p1 = SGGeodesy::direct(bodrumVOR->geod(), 30.0, 16.0 * SG_NM_TO_METER); setPositionAndStabilise(gps, p1); auto gpsNode = globals->get_props()->getNode("instrumentation/gps"); // load into the scratch gpsNode->setStringValue("scratch/query", "BDR"); gpsNode->setStringValue("scratch/type", "vor"); gpsNode->setStringValue("command", "search"); CPPUNIT_ASSERT_EQUAL(true, gpsNode->getBoolValue("scratch/valid")); gpsNode->setStringValue("command", "direct"); CPPUNIT_ASSERT_EQUAL(std::string{"dto"}, std::string{gpsNode->getStringValue("mode")}); CPPUNIT_ASSERT_DOUBLES_EQUAL(p1.getLongitudeDeg(), gpsNode->getDoubleValue("wp/wp[0]/longitude-deg"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(p1.getLatitudeDeg(), gpsNode->getDoubleValue("wp/wp[0]/latitude-deg"), 0.01); CPPUNIT_ASSERT_EQUAL(std::string{"BDR"}, std::string{gpsNode->getStringValue("wp/wp[1]/ID")}); CPPUNIT_ASSERT_DOUBLES_EQUAL(16.0, gpsNode->getDoubleValue("wp/wp[1]/distance-nm"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(210, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(bodrumVOR->get_lon(), gpsNode->getDoubleValue("wp/wp[1]/longitude-deg"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(bodrumVOR->get_lat(), gpsNode->getDoubleValue("wp/wp[1]/latitude-deg"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.1); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); // move off the direct-to line, check everything works SGGeod p2 = SGGeodesy::direct(bodrumVOR->geod(), 35.0, 12.0 * SG_NM_TO_METER); setPositionAndStabilise(gps, p2); CPPUNIT_ASSERT_DOUBLES_EQUAL(5, gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.1); SGGeod p3 = SGGeodesy::direct(p1, 210, 6.0 * SG_NM_TO_METER); SGGeod xtk = SGGeodesy::direct(p3, 120, 0.8 * SG_NM_TO_METER); setPositionAndStabilise(gps, xtk); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.8, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.1); SGGeod xtk2 = SGGeodesy::direct(p3, 120, -1.8 * SG_NM_TO_METER); setPositionAndStabilise(gps, xtk2); CPPUNIT_ASSERT_DOUBLES_EQUAL(-1.8, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.1); } void GPSTests::testNavRadioSlave() { SGPropertyNode_ptr radioConfigNode(new SGPropertyNode); radioConfigNode->setStringValue("name", "navtest"); radioConfigNode->setIntValue("number", 2); std::unique_ptr r(new FGNavRadio(radioConfigNode)); } void GPSTests::testLegMode() { auto rm = globals->get_subsystem(); auto fp = new FlightPlan; rm->setFlightPlan(fp); FGTestApi::setUp::populateFPWithoutNasal(fp, "EBBR", "07L", "EGGD", "27", "NIK COA DVR TAWNY WOD"); // takes the place of the Nasal delegates auto testDelegate = new TestFPDelegate; testDelegate->thePlan = fp; CPPUNIT_ASSERT(rm->activate()); fp->addDelegate(testDelegate); auto gps = setupStandardGPS(); setPositionAndStabilise(gps, fp->departureRunway()->pointOnCenterline(0.0)); auto gpsNode = globals->get_props()->getNode("instrumentation/gps"); gpsNode->setStringValue("command", "leg"); CPPUNIT_ASSERT_EQUAL(std::string{"leg"}, std::string{gpsNode->getStringValue("mode")}); CPPUNIT_ASSERT_EQUAL(std::string{"EBBR-07L"}, std::string{gpsNode->getStringValue("wp/wp[1]/ID")}); CPPUNIT_ASSERT_DOUBLES_EQUAL(65.0, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(65.0, gpsNode->getDoubleValue("desired-course-deg"), 0.5); auto pilot = SGSharedPtr(new FGTestApi::TestPilot); pilot->setSpeedKts(200); pilot->setCourseTrue(65.0); pilot->setTargetAltitudeFtMSL(6000); FGTestApi::runForTime(60.0); // check we sequenced to NIK #if 0 CPPUNIT_ASSERT_EQUAL(1, testDelegate->sequenceCount); #endif CPPUNIT_ASSERT_EQUAL(1, fp->currentIndex()); CPPUNIT_ASSERT_EQUAL(std::string{"EBBR-07L"}, std::string{gpsNode->getStringValue("wp/wp[0]/ID")}); CPPUNIT_ASSERT_EQUAL(std::string{"NIK"}, std::string{gpsNode->getStringValue("wp/wp[1]/ID")}); // reposition along the leg, closer to NIK // and fly to COA SGGeod nikPos = fp->currentLeg()->waypoint()->position(); SGGeod p2 = SGGeodesy::direct(nikPos, 90, 5 * SG_NM_TO_METER); // due east of NIK setPositionAndStabilise(gps, p2); CPPUNIT_ASSERT_DOUBLES_EQUAL(270, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); auto depRwy = fp->departureRunway(); CPPUNIT_ASSERT_DOUBLES_EQUAL(SGGeodesy::distanceNm(nikPos, depRwy->end()), gpsNode->getDoubleValue("wp/leg-distance-nm"), 0.1); const double legCourse = SGGeodesy::courseDeg(depRwy->end(), nikPos); CPPUNIT_ASSERT_DOUBLES_EQUAL(legCourse, gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); const double crsToNIK = SGGeodesy::courseDeg(p2, nikPos); CPPUNIT_ASSERT_DOUBLES_EQUAL(crsToNIK - legCourse, gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); pilot->setSpeedKts(200); pilot->setCourseTrue(270); FGTestApi::runForTime(120.0); #if 0 CPPUNIT_ASSERT_EQUAL(2, testDelegate->sequenceCount); #endif CPPUNIT_ASSERT_EQUAL(2, fp->currentIndex()); CPPUNIT_ASSERT_EQUAL(std::string{"NIK"}, std::string{gpsNode->getStringValue("wp/wp[0]/ID")}); CPPUNIT_ASSERT_EQUAL(std::string{"COA"}, std::string{gpsNode->getStringValue("wp/wp[1]/ID")}); SGGeod coaPos = fp->currentLeg()->waypoint()->position(); CPPUNIT_ASSERT_DOUBLES_EQUAL(SGGeodesy::distanceNm(nikPos, coaPos), gpsNode->getDoubleValue("wp/leg-distance-nm"), 0.1); CPPUNIT_ASSERT_DOUBLES_EQUAL(SGGeodesy::courseDeg(nikPos, coaPos), gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); // check manual sequencing fp->setCurrentIndex(3); CPPUNIT_ASSERT_EQUAL(std::string{"COA"}, std::string{gpsNode->getStringValue("wp/wp[0]/ID")}); CPPUNIT_ASSERT_EQUAL(std::string{"DVR"}, std::string{gpsNode->getStringValue("wp/wp[1]/ID")}); // check course deviation / cross-track error SGGeod doverPos = fp->currentLeg()->waypoint()->position(); double course = SGGeodesy::courseDeg(coaPos, doverPos); double revCourse = SGGeodesy::courseDeg(doverPos, coaPos); setPositionAndStabilise(gps, coaPos); CPPUNIT_ASSERT_DOUBLES_EQUAL(course, gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); SGGeod off1 = SGGeodesy::direct(doverPos, revCourse - 5.0, 16 * SG_NM_TO_METER); setPositionAndStabilise(gps, off1); double courseToDover = SGGeodesy::courseDeg(off1, doverPos); CPPUNIT_ASSERT_DOUBLES_EQUAL(courseToDover, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(-5.0, gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); CPPUNIT_ASSERT_EQUAL(true, gpsNode->getBoolValue("wp/wp[1]/to-flag")); CPPUNIT_ASSERT_EQUAL(false, gpsNode->getBoolValue("wp/wp[1]/from-flag")); SGGeod off2 = SGGeodesy::direct(doverPos, revCourse + 8.0, 40 * SG_NM_TO_METER); setPositionAndStabilise(gps, off2); courseToDover = SGGeodesy::courseDeg(off2, doverPos); CPPUNIT_ASSERT_DOUBLES_EQUAL(courseToDover, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(8.0, gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); CPPUNIT_ASSERT_EQUAL(true, gpsNode->getBoolValue("wp/wp[1]/to-flag")); CPPUNIT_ASSERT_EQUAL(false, gpsNode->getBoolValue("wp/wp[1]/from-flag")); // check cross-track error calculation SGGeod alongTrack = SGGeodesy::direct(coaPos, course, 20 * SG_NM_TO_METER); SGGeod offTrack = SGGeodesy::direct(alongTrack, course + 90, SG_NM_TO_METER * 0.7); setPositionAndStabilise(gps, offTrack); courseToDover = SGGeodesy::courseDeg(offTrack, doverPos); CPPUNIT_ASSERT_DOUBLES_EQUAL(courseToDover, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(-0.7, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.1); SGGeod offTrack2 = SGGeodesy::direct(alongTrack, courseToDover - 90, SG_NM_TO_METER * 3.4); setPositionAndStabilise(gps, offTrack2); CPPUNIT_ASSERT_DOUBLES_EQUAL(3.4, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.1); // check cross track very close to COA SGGeod alongTrack11 = SGGeodesy::direct(coaPos, course, 0.3); SGGeod offTrack25 = SGGeodesy::direct(alongTrack11, course + 90, SG_NM_TO_METER * -3.2); setPositionAndStabilise(gps, offTrack25); CPPUNIT_ASSERT_DOUBLES_EQUAL(3.2, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.1); CPPUNIT_ASSERT_DOUBLES_EQUAL(course, gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.1); // check cross track very close to DVR double distanceCOA_DVR = SGGeodesy::distanceM(coaPos, doverPos); SGGeod alongTrack2 = SGGeodesy::direct(coaPos, course, distanceCOA_DVR - 0.3); SGGeod offTrack3 = SGGeodesy::direct(alongTrack2, course + 90, SG_NM_TO_METER * 1.6); setPositionAndStabilise(gps, offTrack3); CPPUNIT_ASSERT_DOUBLES_EQUAL(-1.6, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.1); CPPUNIT_ASSERT_DOUBLES_EQUAL(revCourse + 180.0, gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.1); // check cross track in the middle SGGeod alongTrack3 = SGGeodesy::direct(coaPos, course, distanceCOA_DVR * 0.52); SGGeod offTrack4 = SGGeodesy::direct(alongTrack3, course + 90, SG_NM_TO_METER * 15.6); setPositionAndStabilise(gps, offTrack4); CPPUNIT_ASSERT_DOUBLES_EQUAL(-15.6, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.1); CPPUNIT_ASSERT_DOUBLES_EQUAL(261.55, gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.1); } void GPSTests::testDirectToLegOnFlightplan() { auto rm = globals->get_subsystem(); auto fp = new FlightPlan; rm->setFlightPlan(fp); FGTestApi::setUp::populateFPWithoutNasal(fp, "EBBR", "07L", "EGGD", "27", "NIK COA DVR TAWNY WOD"); // takes the place of the Nasal delegates auto testDelegate = new TestFPDelegate; testDelegate->thePlan = fp; CPPUNIT_ASSERT(rm->activate()); fp->addDelegate(testDelegate); auto gps = setupStandardGPS(); setPositionAndStabilise(gps, fp->departureRunway()->pointOnCenterline(0.0)); auto gpsNode = globals->get_props()->getNode("instrumentation/gps"); gpsNode->setStringValue("command", "leg"); CPPUNIT_ASSERT_EQUAL(std::string{"leg"}, std::string{gpsNode->getStringValue("mode")}); CPPUNIT_ASSERT_EQUAL(std::string{"EBBR-07L"}, std::string{gpsNode->getStringValue("wp/wp[1]/ID")}); CPPUNIT_ASSERT_DOUBLES_EQUAL(65.0, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(65.0, gpsNode->getDoubleValue("desired-course-deg"), 0.5); // initiate a direct to SGGeod p2 = fp->departureRunway()->pointOnCenterline(5.0* SG_NM_TO_METER); setPositionAndStabilise(gps, p2); auto doverVOR = fp->legAtIndex(3)->waypoint()->source(); double distanceToDover = SGGeodesy::distanceNm(p2, doverVOR->geod()); double bearingToDover = SGGeodesy::courseDeg(p2, doverVOR->geod()); CPPUNIT_ASSERT_EQUAL(std::string{"DVR"}, doverVOR->ident()); gpsNode->setStringValue("scratch/ident", "DVR"); gpsNode->setDoubleValue("scratch/longitude-deg", doverVOR->geod().getLongitudeDeg()); gpsNode->setDoubleValue("scratch/latitude-deg", doverVOR->geod().getLatitudeDeg()); gpsNode->setStringValue("command", "direct"); CPPUNIT_ASSERT_EQUAL(std::string{"dto"}, std::string{gpsNode->getStringValue("mode")}); CPPUNIT_ASSERT_DOUBLES_EQUAL(p2.getLongitudeDeg(), gpsNode->getDoubleValue("wp/wp[0]/longitude-deg"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(p2.getLatitudeDeg(), gpsNode->getDoubleValue("wp/wp[0]/latitude-deg"), 0.01); CPPUNIT_ASSERT_EQUAL(std::string{"DVR"}, std::string{gpsNode->getStringValue("wp/wp[1]/ID")}); CPPUNIT_ASSERT_DOUBLES_EQUAL(distanceToDover, gpsNode->getDoubleValue("wp/wp[1]/distance-nm"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(bearingToDover, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(doverVOR->geod().getLongitudeDeg(), gpsNode->getDoubleValue("wp/wp[1]/longitude-deg"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(doverVOR->geod().getLatitudeDeg(), gpsNode->getDoubleValue("wp/wp[1]/latitude-deg"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.1); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); } void GPSTests::testLongLeg() { auto rm = globals->get_subsystem(); auto fp = new FlightPlan; rm->setFlightPlan(fp); FGTestApi::setUp::populateFPWithoutNasal(fp, "KLAX", "25R", "KJFK", "22R","VNY TEB"); // takes the place of the Nasal delegates auto testDelegate = new TestFPDelegate; testDelegate->thePlan = fp; CPPUNIT_ASSERT(rm->activate()); fp->addDelegate(testDelegate); auto gps = setupStandardGPS(); auto gpsNode = globals->get_props()->getNode("instrumentation/gps"); gpsNode->setStringValue("command", "leg"); // custom CDI deflection output gpsNode->setDoubleValue("config/cdi-max-deflection-nm", 20.0); auto vanNuysVOR = fp->legAtIndex(1)->waypoint()->source(); CPPUNIT_ASSERT_EQUAL(std::string{"VAN NUYS VOR-DME"}, vanNuysVOR->name()); auto teterboroVOR = fp->legAtIndex(2)->waypoint()->source(); CPPUNIT_ASSERT_EQUAL(std::string{"TETERBORO VOR-DME"}, teterboroVOR->name()); fp->setCurrentIndex(2); // initial course at VNY setPositionAndStabilise(gps, vanNuysVOR->geod()); const double initialCourse = SGGeodesy::courseDeg(vanNuysVOR->geod(), teterboroVOR->geod()); const double distanceM = SGGeodesy::distanceM(vanNuysVOR->geod(), teterboroVOR->geod()); CPPUNIT_ASSERT_DOUBLES_EQUAL(initialCourse, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(initialCourse, gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(distanceM * SG_METER_TO_NM, gpsNode->getDoubleValue("wp/wp[1]/distance-nm"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(distanceM * SG_METER_TO_NM, gpsNode->getDoubleValue("wp/leg-distance-nm"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("cdi-deflection"), 0.01); // part of the way, check enroute leg course const SGGeod onTheWay = SGGeodesy::direct(vanNuysVOR->geod(), initialCourse, distanceM * 0.7); setPositionAndStabilise(gps, onTheWay); const double courseNow = SGGeodesy::courseDeg(onTheWay, teterboroVOR->geod()); const double distNow = SGGeodesy::distanceM(onTheWay, teterboroVOR->geod()); CPPUNIT_ASSERT_DOUBLES_EQUAL(courseNow, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(courseNow, gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(distNow * SG_METER_TO_NM, gpsNode->getDoubleValue("wp/wp[1]/distance-nm"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(distanceM * SG_METER_TO_NM, gpsNode->getDoubleValue("wp/leg-distance-nm"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("cdi-deflection"), 0.01); // check a seriously abeam point, we got far of course // to the right of the course, i.e desired track is to our left, so -ve const SGGeod offTheWay = SGGeodesy::direct(onTheWay, courseNow + 90, SG_NM_TO_METER * 13.5); setPositionAndStabilise(gps, offTheWay); const double courseFromOff1 = SGGeodesy::courseDeg(offTheWay, teterboroVOR->geod()); const double dist2 = SGGeodesy::distanceM(offTheWay, teterboroVOR->geod()); CPPUNIT_ASSERT_DOUBLES_EQUAL(courseFromOff1, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(courseNow, gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(-13.5, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(courseFromOff1 - courseNow, gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(dist2 * SG_METER_TO_NM, gpsNode->getDoubleValue("wp/wp[1]/distance-nm"), 0.01); const double expectedDefl1 = -13.5 / 20.0; CPPUNIT_ASSERT_DOUBLES_EQUAL(expectedDefl1 * 10.0, gpsNode->getDoubleValue("cdi-deflection"), 0.01); // check off the other side, close to the destination const SGGeod onTheWay2 = SGGeodesy::direct(vanNuysVOR->geod(), initialCourse, distanceM * 0.92); const double courseOn2 = SGGeodesy::courseDeg(onTheWay2, teterboroVOR->geod()); const SGGeod off2 = SGGeodesy::direct(onTheWay2, courseOn2 - 90, SG_NM_TO_METER * 31.2); setPositionAndStabilise(gps, off2); const double courseFromOff2 = SGGeodesy::courseDeg(off2, teterboroVOR->geod()); const double dist3 = SGGeodesy::distanceM(off2, teterboroVOR->geod()); CPPUNIT_ASSERT_DOUBLES_EQUAL(courseFromOff2, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(courseOn2, gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(31.2, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(courseFromOff2 - courseOn2, gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(dist3 * SG_METER_TO_NM, gpsNode->getDoubleValue("wp/wp[1]/distance-nm"), 0.01); // should peg the CDI, +ve CPPUNIT_ASSERT_DOUBLES_EQUAL(10.0, gpsNode->getDoubleValue("cdi-deflection"), 0.01); } void GPSTests::testLongLegWestbound() { auto rm = globals->get_subsystem(); auto fp = new FlightPlan; rm->setFlightPlan(fp); FGTestApi::setUp::populateFPWithoutNasal(fp, "ENBR", "35", "BIKF", "29","VOO GAKTU"); // takes the place of the Nasal delegates auto testDelegate = new TestFPDelegate; testDelegate->thePlan = fp; CPPUNIT_ASSERT(rm->activate()); fp->addDelegate(testDelegate); auto gps = setupStandardGPS(); auto gpsNode = globals->get_props()->getNode("instrumentation/gps"); gpsNode->setStringValue("command", "leg"); // custom CDI deflection output gpsNode->setDoubleValue("config/cdi-max-deflection-nm", 25.0); auto volloVOR = fp->legAtIndex(1)->waypoint()->source(); CPPUNIT_ASSERT_EQUAL(std::string{"VOLLO VOR-DME"}, volloVOR->name()); auto gaktu = fp->legAtIndex(2)->waypoint()->source(); CPPUNIT_ASSERT_EQUAL(std::string{"GAKTU"}, gaktu->name()); fp->setCurrentIndex(2); // initial course at VNY setPositionAndStabilise(gps, volloVOR->geod()); const double initialCourse = SGGeodesy::courseDeg(volloVOR->geod(), gaktu->geod()); const double distanceM = SGGeodesy::distanceM(volloVOR->geod(), gaktu->geod()); CPPUNIT_ASSERT_DOUBLES_EQUAL(initialCourse, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(initialCourse, gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(distanceM * SG_METER_TO_NM, gpsNode->getDoubleValue("wp/wp[1]/distance-nm"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(distanceM * SG_METER_TO_NM, gpsNode->getDoubleValue("wp/leg-distance-nm"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("cdi-deflection"), 0.01); // part of the way, check enroute leg course const SGGeod onTheWay = SGGeodesy::direct(volloVOR->geod(), initialCourse, distanceM * 0.4); setPositionAndStabilise(gps, onTheWay); const double courseNow = SGGeodesy::courseDeg(onTheWay, gaktu->geod()); const double distNow = SGGeodesy::distanceM(onTheWay, gaktu->geod()); CPPUNIT_ASSERT_DOUBLES_EQUAL(courseNow, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(courseNow, gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(distNow * SG_METER_TO_NM, gpsNode->getDoubleValue("wp/wp[1]/distance-nm"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(distanceM * SG_METER_TO_NM, gpsNode->getDoubleValue("wp/leg-distance-nm"), 0.01); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("cdi-deflection"), 0.01); // check a seriously abeam point, we got far of course // to the right of the course, i.e desired track is to our left, so -ve const SGGeod offTheWay = SGGeodesy::direct(onTheWay, courseNow + 90, SG_NM_TO_METER * 18.6); setPositionAndStabilise(gps, offTheWay); const double courseFromOff1 = SGGeodesy::courseDeg(offTheWay, gaktu->geod()); const double dist2 = SGGeodesy::distanceM(offTheWay, gaktu->geod()); CPPUNIT_ASSERT_DOUBLES_EQUAL(courseFromOff1, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(courseNow, gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(-18.6, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(courseFromOff1 - courseNow, gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(dist2 * SG_METER_TO_NM, gpsNode->getDoubleValue("wp/wp[1]/distance-nm"), 0.01); const double expectedDefl1 = -18.6 / 25.0; CPPUNIT_ASSERT_DOUBLES_EQUAL(expectedDefl1 * 10.0, gpsNode->getDoubleValue("cdi-deflection"), 0.01); } void GPSTests::testOverflightSequencing() { // check that we sequence as we pass over the top // FGTestApi::setUp::logPositionToKML("gps_sequence"); auto rm = globals->get_subsystem(); auto fp = new FlightPlan; rm->setFlightPlan(fp); // let's use New Zealand for some southern hemisphere confusion :) // this route has some deliberately sharp turns to work the turn code FGTestApi::setUp::populateFPWithoutNasal(fp, "NZCH", "02", "NZAA", "05L", "ALADA NS WB WN MAMOD KAPTI OH"); // FGTestApi::writeFlightPlanToKML(fp); // takes the place of the Nasal delegates auto testDelegate = new TestFPDelegate; testDelegate->thePlan = fp; CPPUNIT_ASSERT(rm->activate()); fp->addDelegate(testDelegate); auto gps = setupStandardGPS(); auto gpsNode = globals->get_props()->getNode("instrumentation/gps"); gpsNode->setStringValue("command", "leg"); // very tight tolerance on the overflight. We need a little bit or the // test-piot gets confused right on top gpsNode->setDoubleValue("config/over-flight-distance-nm", 0.01); // enroute to NELSON VOR fp->setCurrentIndex(2); SGGeod pos = fp->pointAlongRoute(2, -5.0); setPositionAndStabilise(gps, pos); auto pilot = SGSharedPtr(new FGTestApi::TestPilot); pilot->resetAtPosition(pos); pilot->setSpeedKts(300); // decent speed to make things tougher pilot->flyGPSCourse(gps); bool ok = FGTestApi::runForTimeWithCheck(180.0, [fp] () { if (fp->currentIndex() == 3) { return true; } return false; }); CPPUNIT_ASSERT(ok); // check we're on top of NELSON auto nelsonVOR = fp->legAtIndex(2)->waypoint()->source(); CPPUNIT_ASSERT_EQUAL(std::string{"NELSON VOR-DME"}, nelsonVOR->name()); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, SGGeodesy::distanceNm(globals->get_aircraft_position(), nelsonVOR->geod()), 0.1); FGTestApi::runForTime(120.0); // check we're on course to Woodburne auto woodbourneVOR = fp->legAtIndex(3)->waypoint()->source(); const double crsToWB = SGGeodesy::courseDeg(globals->get_aircraft_position(), woodbourneVOR->geod()); const double crsNSWB = SGGeodesy::courseDeg(nelsonVOR->geod(), woodbourneVOR->geod()); CPPUNIT_ASSERT_DOUBLES_EQUAL(crsToWB, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(crsNSWB, gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); // point a bit before MAMOD SGGeod pos2 = fp->pointAlongRoute(5, -5.0); setPositionAndStabilise(gps, pos2); fp->setCurrentIndex(5); auto mamod = fp->legAtIndex(5)->waypoint()->source(); ok = FGTestApi::runForTimeWithCheck(180.0, [fp] () { if (fp->currentIndex() == 6) { return true; } return false; }); CPPUNIT_ASSERT(ok); // check we're on top of MAMOD CPPUNIT_ASSERT_EQUAL(std::string{"MAMOD"}, mamod->name()); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, SGGeodesy::distanceNm(globals->get_aircraft_position(), mamod->geod()), 0.1); FGTestApi::runForTime(180.0); // check we're on course to KAPTI auto kapti = fp->legAtIndex(6)->waypoint()->source(); const double crsKapti = SGGeodesy::courseDeg(globals->get_aircraft_position(), kapti->geod()); const double crsMamodKapti = SGGeodesy::courseDeg(mamod->geod(), kapti->geod()); CPPUNIT_ASSERT_DOUBLES_EQUAL(crsKapti, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(crsMamodKapti, gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); } void GPSTests::testOffcourseSequencing() { // ensure that if we pass through the overflight arm cone, but not // over the waypoint direclty, we still sequence at the mid-point // FGTestApi::setUp::logPositionToKML("gps_sequence_off"); auto rm = globals->get_subsystem(); auto fp = new FlightPlan; rm->setFlightPlan(fp); FGTestApi::setUp::populateFPWithoutNasal(fp, "NZCH", "02", "NZAA", "05L", "ALADA NS WB WN MAMOD KAPTI OH"); // FGTestApi::writeFlightPlanToKML(fp); // takes the place of the Nasal delegates auto testDelegate = new TestFPDelegate; testDelegate->thePlan = fp; CPPUNIT_ASSERT(rm->activate()); fp->addDelegate(testDelegate); auto gps = setupStandardGPS(); auto gpsNode = globals->get_props()->getNode("instrumentation/gps"); gpsNode->setStringValue("command", "leg"); gpsNode->setDoubleValue("config/over-flight-distance-nm", 0.01); // enroute to NELSON VOR fp->setCurrentIndex(2); SGGeod pos = fp->pointAlongRoute(2, -5.0); pos = SGGeodesy::direct(pos, 180, 1.5 * SG_NM_TO_METER); setPositionAndStabilise(gps, pos); auto nelsonVOR = fp->legAtIndex(2)->waypoint()->source(); CPPUNIT_ASSERT_EQUAL(std::string{"NELSON VOR-DME"}, nelsonVOR->name()); auto pilot = SGSharedPtr(new FGTestApi::TestPilot); pilot->resetAtPosition(pos); pilot->setSpeedKts(300); // decent speed to make things tougher // since we're south of the route, but will fly parallel to it, we won't // pass over NELSON directly. const double legCourse = gpsNode->getDoubleValue("wp/leg-true-course-deg"); pilot->turnToCourse(legCourse); bool ok = FGTestApi::runForTimeWithCheck(180.0, [fp] () { if (fp->currentIndex() == 3) { return true; } return false; }); CPPUNIT_ASSERT(ok); // check we're close NELSON const double distToNELSON = SGGeodesy::distanceNm(globals->get_aircraft_position(), nelsonVOR->geod()); CPPUNIT_ASSERT(distToNELSON < 1.0); const double bearingToNELSON = SGGeodesy::courseDeg(globals->get_aircraft_position(), nelsonVOR->geod()); // find the inbound course from ALADA at NELSON double finalLegTrack = SGGeodesy::courseDeg(nelsonVOR->geod(), fp->legAtIndex(1)->waypoint()->position()) + 180.0; SG_NORMALIZE_RANGE(finalLegTrack, 0.0, 360.0); double dev = bearingToNELSON - finalLegTrack; SG_NORMALIZE_RANGE(dev, -180.0, 180.0); double absDeviation = fabs(dev); // 90 is the overflight arm angle, i.e we sequence as soon as the waypoint is abeam us CPPUNIT_ASSERT_DOUBLES_EQUAL(90.0, absDeviation, 1.0); pilot->flyGPSCourse(gps); FGTestApi::runForTime(120.0); // check we're on course to Woodburne auto woodbourneVOR = fp->legAtIndex(3)->waypoint()->source(); const double crsToWB = SGGeodesy::courseDeg(globals->get_aircraft_position(), woodbourneVOR->geod()); const double crsNSWB = SGGeodesy::courseDeg(nelsonVOR->geod(), woodbourneVOR->geod()); CPPUNIT_ASSERT_DOUBLES_EQUAL(crsToWB, gpsNode->getDoubleValue("wp/wp[1]/bearing-true-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(crsNSWB, gpsNode->getDoubleValue("wp/leg-true-course-deg"), 0.5); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.05); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg"), 0.5); // let's go wide at MAMOD SGGeod pos2 = fp->pointAlongRoute(5, -5.0); pos2 = SGGeodesy::direct(pos2, 180, 0.8 * SG_NM_TO_METER); fp->setCurrentIndex(5); setPositionAndStabilise(gps, pos2); // start facing the right way pilot->setCourseTrue(gpsNode->getDoubleValue("wp/leg-true-course-deg")); pilot->flyHeading(gpsNode->getDoubleValue("wp/leg-true-course-deg")); auto mamod = fp->legAtIndex(5)->waypoint()->source(); ok = FGTestApi::runForTimeWithCheck(180.0, [fp] () { if (fp->currentIndex() == 6) { return true; } return false; }); CPPUNIT_ASSERT(ok); // check we're on top of MAMOD CPPUNIT_ASSERT_EQUAL(std::string{"MAMOD"}, mamod->name()); const double distToMAMOD = SGGeodesy::distanceNm(globals->get_aircraft_position(), mamod->geod()); CPPUNIT_ASSERT(distToMAMOD < 1.0); // check the angle we sequenced at const double bearingToMAMOD = SGGeodesy::courseDeg(globals->get_aircraft_position(), mamod->geod()); // find the inbound course from WELLINGTON at MAMOD finalLegTrack = SGGeodesy::courseDeg(mamod->geod(), fp->legAtIndex(4)->waypoint()->position()) + 180.0; SG_NORMALIZE_RANGE(finalLegTrack, 0.0, 360.0); dev = bearingToMAMOD - finalLegTrack; SG_NORMALIZE_RANGE(dev, -180.0, 180.0); absDeviation = fabs(dev); // 90 is the overflight arm angle, i.e we sequence as soon as the waypoint is abeam us CPPUNIT_ASSERT_DOUBLES_EQUAL(90.0, absDeviation, 1.0); // check we don't sequence, if we're too far off course SGGeod pos3 = fp->pointAlongRoute(6, -5.0); pos3 = SGGeodesy::direct(pos3, 90, 1.8 * SG_NM_TO_METER); fp->setCurrentIndex(6); setPositionAndStabilise(gps, pos3); // start facing the right way pilot->setCourseTrue(gpsNode->getDoubleValue("wp/leg-true-course-deg")); pilot->flyHeading(gpsNode->getDoubleValue("wp/leg-true-course-deg")); ok = FGTestApi::runForTimeWithCheck(180.0, [fp] () { if (fp->currentIndex() != 6) { CPPUNIT_ASSERT(false); return true; } return false; }); } void GPSTests::testOffsetFlight() { // verify the XTK value during waypoint transitions, especially that we // don't get any weird discontinuities // FGTestApi::setUp::logPositionToKML("gps_offset_flight"); auto rm = globals->get_subsystem(); auto fp = new FlightPlan; rm->setFlightPlan(fp); FGTestApi::setUp::populateFPWithoutNasal(fp, "UHMA", "01", "PAMR", "25", "SOMEG BE KIVAK BET"); // FGTestApi::writeFlightPlanToKML(fp); // takes the place of the Nasal delegates auto testDelegate = new TestFPDelegate; testDelegate->thePlan = fp; CPPUNIT_ASSERT(rm->activate()); fp->addDelegate(testDelegate); auto gps = setupStandardGPS(); auto gpsNode = globals->get_props()->getNode("instrumentation/gps"); gpsNode->setStringValue("command", "leg"); gpsNode->setDoubleValue("config/over-flight-distance-nm", 0.01); // enroute to BERINGOVSKY NDB fp->setCurrentIndex(2); SGGeod pos = fp->pointAlongRoute(2, -10.0); pos = SGGeodesy::direct(pos, 180, 2.0 * SG_NM_TO_METER); setPositionAndStabilise(gps, pos); auto pilot = SGSharedPtr(new FGTestApi::TestPilot); pilot->resetAtPosition(pos); pilot->setSpeedKts(300); // decent speed to make things tougher // start facing the right way pilot->setCourseTrue(gpsNode->getDoubleValue("wp/leg-true-course-deg")); pilot->flyGPSCourseOffset(gps, 0.8); FGTestApi::runForTime(90.0); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.8, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.1); bool ok = FGTestApi::runForTimeWithCheck(180.0, [fp] () { if (fp->currentIndex() == 3) { return true; } return false; }); CPPUNIT_ASSERT(ok); FGTestApi::runForTime(120.0); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.8, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.1); // fly until we cross the awkward line :) ok = FGTestApi::runForTimeWithCheck(6000.0, [] () { const double lon = globals->get_aircraft_position().getLongitudeDeg(); if ((lon > -178.0) && (lon < -170.0)) { return true; } return false; }); CPPUNIT_ASSERT(ok); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.8, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.1); // and try an inside corrner fp->setCurrentIndex(3); pos = fp->pointAlongRoute(3, -18.0); pos = SGGeodesy::direct(pos, 180, 2.0 * SG_NM_TO_METER); setPositionAndStabilise(gps, pos); pilot->resetAtPosition(pos); // start facing the right way pilot->setCourseTrue(gpsNode->getDoubleValue("wp/leg-true-course-deg")); FGTestApi::runForTime(120.0); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.8, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.1); ok = FGTestApi::runForTimeWithCheck(180.0, [fp] () { if (fp->currentIndex() == 4) { return true; } return false; }); CPPUNIT_ASSERT(ok); FGTestApi::runForTime(120.0); CPPUNIT_ASSERT_DOUBLES_EQUAL(0.8, gpsNode->getDoubleValue("wp/wp[1]/course-error-nm"), 0.1); } void GPSTests::testTurnAnticipation() { }