/*
* Copyright (C) 2020 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 "config.h"
#include "test_flightplan.hxx"
#include
#include "test_suite/FGTestApi/testGlobals.hxx"
#include "test_suite/FGTestApi/NavDataCache.hxx"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std::string_literals;
using namespace flightgear;
static bool static_haveProcedures = false;
/////////////////////////////////////////////////////////////////////////////
namespace {
class TestFPDelegate : public FlightPlan::Delegate
{
public:
TestFPDelegate(FlightPlan* plan) :
_plan(plan)
{}
virtual ~TestFPDelegate()
{
}
void sequence() override
{
}
void currentWaypointChanged() override
{
}
void waypointsChanged() override
{
sawWaypointsChange = true;
}
void departureChanged() override
{
sawDepartureChange = true;
// mimic the default delegate, inserting the SID waypoints
// clear anything existing
_plan->clearWayptsWithFlag(WPT_DEPARTURE);
// insert waypt for the dpearture runway
auto dr = new RunwayWaypt(_plan->departureRunway(), _plan);
dr->setFlag(WPT_DEPARTURE);
dr->setFlag(WPT_GENERATED);
_plan->insertWayptAtIndex(dr, 0);
if (_plan->sid()) {
WayptVec sidRoute;
bool ok = _plan->sid()->route(_plan->departureRunway(), _plan->sidTransition(), sidRoute);
if (!ok)
throw sg_exception("failed to route via SID");
int insertIndex = 1;
for (auto w : sidRoute) {
w->setFlag(WPT_DEPARTURE);
w->setFlag(WPT_GENERATED);
_plan->insertWayptAtIndex(w, insertIndex++);
}
}
}
void arrivalChanged() override
{
sawArrivalChange = true;
// mimic the default delegate, inserting the STAR waypoints
// clear anything existing
_plan->clearWayptsWithFlag(WPT_ARRIVAL);
if (!_plan->destinationAirport()) {
return;
}
if (!_plan->destinationRunway()) {
auto ap = new NavaidWaypoint(_plan->destinationAirport(), _plan);
ap->setFlag(WPT_ARRIVAL);
ap->setFlag(WPT_GENERATED);
_plan->insertWayptAtIndex(ap, -1);
return;
}
// insert waypt for the destination runway
auto dr = new RunwayWaypt(_plan->destinationRunway(), _plan);
dr->setFlag(WPT_ARRIVAL);
dr->setFlag(WPT_GENERATED);
auto leg = _plan->insertWayptAtIndex(dr, -1);
if (_plan->star()) {
WayptVec starRoute;
bool ok = _plan->star()->route(_plan->destinationRunway(), _plan->starTransition(), starRoute);
if (!ok)
throw sg_exception("failed to route via STAR");
int insertIndex = leg->index();
for (auto w : starRoute) {
w->setFlag(WPT_ARRIVAL);
w->setFlag(WPT_GENERATED);
_plan->insertWayptAtIndex(w, insertIndex++);
}
}
if (_plan->approach()) {
WayptVec approachRoute;
bool ok = _plan->approach()->routeFromVectors(approachRoute);
if (!ok)
throw sg_exception("failed to route approach");
int insertIndex = leg->index();
for (auto w : approachRoute) {
w->setFlag(WPT_ARRIVAL);
w->setFlag(WPT_GENERATED);
_plan->insertWayptAtIndex(w, insertIndex++);
}
}
}
void loaded() override
{
didLoad = true;
}
FlightPlan* _plan = nullptr;
bool didLoad = false;
bool sawDepartureChange = false;
bool sawArrivalChange = false;
bool sawWaypointsChange = false;
};
class TestFPDelegateFactory : public FlightPlan::DelegateFactory
{
public:
TestFPDelegateFactory() = default;
virtual ~TestFPDelegateFactory() = default;
FlightPlan::Delegate* createFlightPlanDelegate(FlightPlan* fp) override
{
auto d = new TestFPDelegate(fp);
_instances.push_back(d);
return d;
}
void destroyFlightPlanDelegate(FlightPlan* fp, FlightPlan::Delegate* d) override
{
auto it = std::find_if(_instances.begin(), _instances.end(), [d] (TestFPDelegate* fpd) {
return fpd == d;
});
CPPUNIT_ASSERT(it != _instances.end());
_instances.erase(it);
delete d;
}
static TestFPDelegate* delegateForPlan(FlightPlan* fp);
std::vector _instances;
};
std::shared_ptr static_factory;
TestFPDelegate* TestFPDelegateFactory::delegateForPlan(FlightPlan* fp)
{
auto it = std::find_if(static_factory->_instances.begin(),
static_factory->_instances.end(),
[fp] (TestFPDelegate* del) {
return del->_plan == fp;
});
if (it == static_factory->_instances.end())
return nullptr;
return *it;
}
} // of anonymous namespace
/////////////////////////////////////////////////////////////////////////////
// Set up function for each test.
void FlightplanTests::setUp()
{
FGTestApi::setUp::initTestGlobals("flightplan"s);
FGTestApi::setUp::initNavDataCache();
SGPath proceduresPath = SGPath::fromEnv("FG_PROCEDURES_PATH");
if (proceduresPath.exists()) {
static_haveProcedures = true;
globals->append_fg_scenery(proceduresPath);
}
globals->get_subsystem_mgr()->bind();
globals->get_subsystem_mgr()->init();
globals->get_subsystem_mgr()->postinit();
}
// Clean up after each test.
void FlightplanTests::tearDown()
{
FGTestApi::tearDown::shutdownTestGlobals();
if (static_factory) {
FlightPlan::unregisterDelegateFactory(static_factory);
static_factory.reset();
}
}
static FlightPlanRef makeTestFP(const std::string& depICAO, const std::string& depRunway,
const std::string& destICAO, const std::string& destRunway,
const std::string& waypoints, bool asRoute = false)
{
FlightPlanRef f = asRoute ? FlightPlan::createRoute() : FlightPlan::create();
FGTestApi::setUp::populateFPWithoutNasal(f, depICAO, depRunway, destICAO, destRunway, waypoints);
return f;
}
void FlightplanTests::testBasic()
{
FlightPlanRef fp1 = makeTestFP("EGCC"s, "23L"s, "EHAM"s, "24"s,
"TNT CLN"s);
fp1->setIdent("testplan"s);
CPPUNIT_ASSERT(fp1->ident() == "testplan"s);
CPPUNIT_ASSERT(fp1->departureAirport()->ident() == "EGCC"s);
CPPUNIT_ASSERT(fp1->departureRunway()->ident() == "23L"s);
CPPUNIT_ASSERT(fp1->destinationAirport()->ident() == "EHAM"s);
CPPUNIT_ASSERT(fp1->destinationRunway()->ident() == "24"s);
CPPUNIT_ASSERT_EQUAL(5, fp1->numLegs());
CPPUNIT_ASSERT(fp1->legAtIndex(0)->waypoint()->source()->ident() == "23L"s);
CPPUNIT_ASSERT(fp1->legAtIndex(1)->waypoint()->source()->ident() == "TNT"s);
CPPUNIT_ASSERT(fp1->legAtIndex(1)->waypoint()->source()->name() == "TRENT VOR-DME"s);
CPPUNIT_ASSERT(fp1->legAtIndex(2)->waypoint()->source()->ident() == "CLN"s);
CPPUNIT_ASSERT(fp1->legAtIndex(2)->waypoint()->source()->name() == "CLACTON VOR-DME"s);
CPPUNIT_ASSERT(fp1->legAtIndex(4)->waypoint()->source()->ident() == "24"s);
}
void FlightplanTests::testRoutePathBasic()
{
FlightPlanRef fp1 = makeTestFP("EGHI"s, "20"s, "EDDM"s, "08L"s,
"SFD LYD BNE CIV ELLX LUX SAA KRH WLD"s);
RoutePath rtepath(fp1);
const int legCount = fp1->numLegs();
for (int leg = 0; leg < legCount; ++leg) {
rtepath.trackForIndex(leg);
rtepath.pathForIndex(leg);
rtepath.distanceForIndex(leg);
}
rtepath.distanceBetweenIndices(2, 5);
// check some leg parameters
// BOLOUGNE SUR MER, near LFAY (AMIENS)
FGNavRecordRef bne = FGNavList::findByFreq(113.8, FGAirport::getByIdent("LFAY"s)->geod());
// CHIEVRES
FGNavRecordRef civ = FGNavList::findByFreq(113.2, FGAirport::getByIdent("EBCI"s)->geod());
double distM = SGGeodesy::distanceM(bne->geod(), civ->geod());
double trackDeg = SGGeodesy::courseDeg(bne->geod(), civ->geod());
CPPUNIT_ASSERT_DOUBLES_EQUAL(trackDeg, rtepath.trackForIndex(4), 0.5);
CPPUNIT_ASSERT_DOUBLES_EQUAL(distM, rtepath.distanceForIndex(4), 2000); // 2km precision, allow for turns
}
// https://sourceforge.net/p/flightgear/codetickets/1703/
// https://sourceforge.net/p/flightgear/codetickets/1939/
void FlightplanTests::testRoutePathSkipped()
{
FlightPlanRef fp1 = makeTestFP("EHAM"s, "24"s, "EDDM"s, "08L"s,
"EHEH KBO TAU FFM FFM/100/0.01 FFM/120/0.02 WUR WLD"s);
RoutePath rtepath(fp1);
// skipped point uses inbound track
CPPUNIT_ASSERT_EQUAL(rtepath.trackForIndex(4), rtepath.trackForIndex(5));
CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, rtepath.distanceForIndex(5), 1e-9);
CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, rtepath.distanceForIndex(6), 1e-9);
CPPUNIT_ASSERT_DOUBLES_EQUAL(101000, rtepath.distanceForIndex(7), 1000);
// this tests skipping two preceeding points works as it should
SGGeodVec vec = rtepath.pathForIndex(7);
CPPUNIT_ASSERT(vec.size() == 9);
}
void FlightplanTests::testRoutePathTrivialFlightPlan()
{
FlightPlanRef fp1 = makeTestFP("EGPH"s, "24"s, "EGPH"s, "06"s,
""s);
RoutePath rtepath(fp1);
const int legCount = fp1->numLegs();
for (int leg = 0; leg < legCount; ++leg) {
rtepath.trackForIndex(leg);
rtepath.pathForIndex(leg);
rtepath.distanceForIndex(leg);
}
CPPUNIT_ASSERT_DOUBLES_EQUAL(19.5, fp1->totalDistanceNm(), 0.1);
}
void FlightplanTests::testRoutePathVec()
{
FlightPlanRef fp1 = makeTestFP("KNUQ"s, "14L"s, "PHNL"s, "22R"s,
"ROKME WOVAB"s);
RoutePath rtepath(fp1);
SGGeodVec vec = rtepath.pathForIndex(0);
FGAirportRef ksfo = FGAirport::findByIdent("KSFO"s);
FGFixRef rokme = fgpositioned_cast(FGPositioned::findClosestWithIdent("ROKME"s, ksfo->geod()));
auto depRwy = fp1->departureRunway();
CPPUNIT_ASSERT_DOUBLES_EQUAL(depRwy->geod().getLongitudeDeg(), vec.front().getLongitudeDeg(), 0.01);
CPPUNIT_ASSERT_DOUBLES_EQUAL(depRwy->geod().getLatitudeDeg(), vec.front().getLatitudeDeg(), 0.01);
SGGeodVec vec1 = rtepath.pathForIndex(1);
CPPUNIT_ASSERT_DOUBLES_EQUAL(rokme->geod().getLongitudeDeg(), vec1.back().getLongitudeDeg(), 0.01);
CPPUNIT_ASSERT_DOUBLES_EQUAL(rokme->geod().getLatitudeDeg(), vec1.back().getLatitudeDeg(), 0.01);
SGGeodVec vec2 = rtepath.pathForIndex(2);
CPPUNIT_ASSERT_DOUBLES_EQUAL(rokme->geod().getLongitudeDeg(), vec2.front().getLongitudeDeg(), 0.01);
CPPUNIT_ASSERT_DOUBLES_EQUAL(rokme->geod().getLatitudeDeg(), vec2.front().getLatitudeDeg(), 0.01);
//CPPUNIT_ASSERT(vec.front()
}
void FlightplanTests::testRoutePathFinalLegVQPR15()
{
// test behaviour of RoutePath when the last leg prior to the arrival runway
// is beyond the runway. This occurs in Paro RNAVZ15 approach.
if (!static_haveProcedures)
return;
static_factory = std::make_shared();
FlightPlan::registerDelegateFactory(static_factory);
FlightPlanRef f = FlightPlan::create();
// auto ourDelegate = TestFPDelegateFactory::delegateForPlan(f);
auto vidp = FGAirport::findByIdent("VIDP"s);
f->setDeparture(vidp->getRunwayByIdent("09"s));
auto vqpr = FGAirport::findByIdent("VQPR"s);
f->setDestination(vqpr->getRunwayByIdent("15"s));
f->setApproach(vqpr->findApproachWithIdent("RNVZ15"s));
RoutePath rtepath(f);
CPPUNIT_ASSERT_DOUBLES_EQUAL(1100, rtepath.distanceForIndex(14), 100); // calculated from raw lat lon.
}
void FlightplanTests::testRoutPathWpt0Midflight()
{
// test behaviour of RoutePath when WP0 is not a runway
// happens for the Airbus ND which removes past wpts when sequencing
FlightPlanRef fp1 = makeTestFP("KNUQ"s, "14L"s, "PHNL"s, "22R"s,
"ROKME WOVAB"s);
// actually delete leg 0 so we start at ROKME
fp1->deleteIndex(0);
RoutePath rtepath(fp1);
SGGeodVec vec = rtepath.pathForIndex(0);
FGAirportRef ksfo = FGAirport::findByIdent("KSFO"s);
FGFixRef rokme = fgpositioned_cast(FGPositioned::findClosestWithIdent("ROKME"s, ksfo->geod()));
CPPUNIT_ASSERT_DOUBLES_EQUAL(rokme->geod().getLongitudeDeg(), vec.front().getLongitudeDeg(), 0.01);
CPPUNIT_ASSERT_DOUBLES_EQUAL(rokme->geod().getLatitudeDeg(), vec.front().getLatitudeDeg(), 0.01);
SGGeodVec vec2 = rtepath.pathForIndex(1);
CPPUNIT_ASSERT_DOUBLES_EQUAL(rokme->geod().getLongitudeDeg(), vec2.front().getLongitudeDeg(), 0.01);
CPPUNIT_ASSERT_DOUBLES_EQUAL(rokme->geod().getLatitudeDeg(), vec2.front().getLatitudeDeg(), 0.01);
//CPPUNIT_ASSERT(vec.front()
}
void FlightplanTests::testBasicAirways()
{
Airway* awy = Airway::findByIdent("J547"s, Airway::HighLevel);
CPPUNIT_ASSERT_EQUAL(awy->ident(), "J547"s);
FGAirportRef kord = FGAirport::findByIdent("KORD"s);
FlightPlanRef f = FlightPlan::create();
f->setDeparture(kord);
CPPUNIT_ASSERT(awy->findEnroute("KITOK"s));
CPPUNIT_ASSERT(awy->findEnroute("LESUB"s));
auto wpt = awy->findEnroute("FNT"s);
CPPUNIT_ASSERT(wpt);
CPPUNIT_ASSERT(wpt->ident() == "FNT"s);
CPPUNIT_ASSERT(wpt->source() == FGNavRecord::findClosestWithIdent("FNT"s, kord->geod()));
auto wptKUBBS = f->waypointFromString("KUBBS"s);
auto wptFNT = f->waypointFromString("FNT"s);
CPPUNIT_ASSERT(awy->canVia(wptKUBBS, wptFNT));
WayptVec path = awy->via(wptKUBBS, wptFNT);
CPPUNIT_ASSERT_EQUAL(4, static_cast(path.size()));
CPPUNIT_ASSERT_EQUAL("PMM"s, path.at(0)->ident());
CPPUNIT_ASSERT_EQUAL("HASTE"s, path.at(1)->ident());
CPPUNIT_ASSERT_EQUAL("DEWIT"s, path.at(2)->ident());
CPPUNIT_ASSERT_EQUAL("FNT"s, path.at(3)->ident());
f->insertWayptsAtIndex(path, 0);
CPPUNIT_ASSERT_EQUAL(4, static_cast(f->numLegs()));
CPPUNIT_ASSERT_EQUAL("DEWIT"s, f->legAtIndex(2)->waypoint()->ident());
CPPUNIT_ASSERT_EQUAL(static_cast(awy), f->legAtIndex(2)->waypoint()->owner());
}
void FlightplanTests::testViaInsertIntoFP()
{
Airway* awy = Airway::findByIdent("J547"s, Airway::HighLevel);
CPPUNIT_ASSERT_EQUAL(awy->ident(), "J547"s);
FGAirportRef kord = FGAirport::findByIdent("KORD"s);
FlightPlanRef f = FlightPlan::create();
f->setDeparture(kord);
CPPUNIT_ASSERT(awy->findEnroute("KITOK"s));
CPPUNIT_ASSERT(awy->findEnroute("LESUB"s));
auto wpt = awy->findEnroute("FNT"s);
CPPUNIT_ASSERT(wpt);
auto wptKUBBS = f->waypointFromString("KUBBS"s);
auto wptFNT = f->waypointFromString("FNT"s);
f->insertWayptAtIndex(wptKUBBS, -1);
CPPUNIT_ASSERT_EQUAL(1, static_cast(f->numLegs()));
Via* via = new Via(f, awy, wptFNT->source());
f->insertWayptAtIndex(via, -1);
CPPUNIT_ASSERT_EQUAL(5, static_cast(f->numLegs()));
CPPUNIT_ASSERT_EQUAL("PMM"s, f->legAtIndex(1)->waypoint()->ident());
CPPUNIT_ASSERT_EQUAL(static_cast(awy), f->legAtIndex(1)->waypoint()->owner());
}
void FlightplanTests::testViaInsertIntoRoute()
{
Airway* awy = Airway::findByIdent("J547"s, Airway::HighLevel);
CPPUNIT_ASSERT_EQUAL(awy->ident(), "J547"s);
FGAirportRef kord = FGAirport::findByIdent("KORD"s);
FlightPlanRef f = FlightPlan::createRoute();
f->setDeparture(kord);
CPPUNIT_ASSERT(awy->findEnroute("KITOK"s));
CPPUNIT_ASSERT(awy->findEnroute("LESUB"s));
auto wpt = awy->findEnroute("FNT"s);
CPPUNIT_ASSERT(wpt);
auto wptKUBBS = f->waypointFromString("KUBBS"s);
auto wptFNT = f->waypointFromString("FNT"s);
f->insertWayptAtIndex(wptKUBBS, -1);
CPPUNIT_ASSERT_EQUAL(1, static_cast(f->numLegs()));
Via* via = new Via(f, awy, wptFNT->source());
f->insertWayptAtIndex(via, -1);
CPPUNIT_ASSERT_EQUAL(2, static_cast(f->numLegs()));
// clone and convert to active flight-plan; Vias
// should be expanded
FlightPlanRef activeFP = f->clone("ACT", true);
CPPUNIT_ASSERT_EQUAL(5, static_cast(activeFP->numLegs()));
CPPUNIT_ASSERT_EQUAL("PMM"s, activeFP->legAtIndex(1)->waypoint()->ident());
CPPUNIT_ASSERT_EQUAL(static_cast(awy), activeFP->legAtIndex(1)->waypoint()->owner());
}
void FlightplanTests::testAirwayNetworkRoute()
{
FGAirportRef egph = FGAirport::findByIdent("EGPH"s);
FlightPlanRef f = FlightPlan::create();
f->setDeparture(egph);
auto highLevelNet = Airway::highLevel();
auto wptTLA = f->waypointFromString("TLA"s);
auto wptCNA = f->waypointFromString("CNA"s);
WayptVec route;
bool ok = highLevelNet->route(wptTLA, wptCNA, route);
CPPUNIT_ASSERT(ok);
CPPUNIT_ASSERT_EQUAL(static_cast(route.size()), 18);
}
void FlightplanTests::testParseICAORoute()
{
FGAirportRef kord = FGAirport::findByIdent("KORD"s);
FlightPlanRef f = FlightPlan::create();
f->setDeparture(kord);
f->setDestination(FGAirport::findByIdent("KSAN"s));
const char* route = "DCT JOT J26 IRK J96 SLN J18 GCK J96 CIM J134 GUP J96 KEYKE J134 DRK J78 LANCY J96 PKE";
// const char* route = "DCT KUBBS J547 FNT Q824 HOCKE Q905 SIKBO Q907 MIILS N400A TUDEP NATW GISTI DCT SLANY UL9 DIKAS UL18 GAVGO UL9 KONAN UL607 UBIDU Y863 RUDUS T109 HAREM T104 ANORA STAR";
bool ok = f->parseICAORouteString(route);
CPPUNIT_ASSERT(ok);
}
void FlightplanTests::testParseICANLowLevelRoute()
{
const char* route = "DCT DPA V6 IOW V216 LAA V210 GOSIP V83 ACHES V210 BLOKE V83 ALS V210 RSK V95 INW V12 HOXOL V264 OATES V12 JUWSO V264 PKE";
FGAirportRef kord = FGAirport::findByIdent("KORD"s);
FlightPlanRef f = FlightPlan::create();
f->setDeparture(kord);
f->setDestination(FGAirport::findByIdent("KSAN"s));
bool ok = f->parseICAORouteString(route);
CPPUNIT_ASSERT(ok);
}
// https://sourceforge.net/p/flightgear/codetickets/1814/
void FlightplanTests::testBug1814()
{
const std::string fpXML = R"(
2
SAWG
25
SUMU
navaid
PUGLI
-60.552200
-40.490000
navaid
SIGUL
-59.655800
-38.312800
navaid
MDP
-57.576400
-37.929800
)";
std::istringstream stream(fpXML);
FlightPlanRef f = FlightPlan::create();
bool ok = f->load(stream);
CPPUNIT_ASSERT(ok);
auto leg = f->legAtIndex(1);
CPPUNIT_ASSERT_EQUAL("SIGUL"s, leg->waypoint()->ident());
CPPUNIT_ASSERT_DOUBLES_EQUAL(137, leg->distanceNm(), 0.5);
CPPUNIT_ASSERT_DOUBLES_EQUAL(101, f->legAtIndex(2)->distanceNm(), 0.5);
}
void FlightplanTests::testLoadSaveMachRestriction()
{
const std::string fpXML = R"(
2
SAWG
25
SUMU
navaid
PUGLI
-60.552200
-40.490000
basic
at
36000
mach
1.24
SV002
-115.50531
37.89523
navaid
SIGUL
-60.552200
-40.490000
)";
std::istringstream stream(fpXML);
FlightPlanRef f = FlightPlan::create();
bool ok = f->load(stream);
CPPUNIT_ASSERT(ok);
auto leg = f->legAtIndex(1);
CPPUNIT_ASSERT_EQUAL(SPEED_RESTRICT_MACH, leg->speedRestriction());
CPPUNIT_ASSERT_DOUBLES_EQUAL(1.24, leg->speedMach(), 0.01);
auto firstLeg = f->legAtIndex(0);
firstLeg->setSpeed(SPEED_RESTRICT_MACH, 1.56);
// upgrade to a hold and set the count
f->legAtIndex(2)->setHoldCount(8);
// round trip through XML to check :)
std::ostringstream ss;
f->save(ss);
std::istringstream iss(ss.str());
FlightPlanRef f2 = FlightPlan::create();
ok = f2->load(iss);
CPPUNIT_ASSERT(ok);
auto leg3 = f2->legAtIndex(0);
CPPUNIT_ASSERT_EQUAL(SPEED_RESTRICT_MACH, leg3->speedRestriction());
CPPUNIT_ASSERT_DOUBLES_EQUAL(1.56, leg3->speedMach(), 0.01);
CPPUNIT_ASSERT_EQUAL(8, f2->legAtIndex(2)->holdCount());
}
void FlightplanTests::testBasicDiscontinuity()
{
FlightPlanRef fp1 = makeTestFP("LIRF"s, "34L"s, "LEBL"s, "25R"s,
"ESINO GITRI BALEN MUREN TOSNU"s);
const auto tdBefore = fp1->totalDistanceNm();
const SGGeod balenPos = fp1->legAtIndex(3)->waypoint()->position();
const SGGeod murenPos = fp1->legAtIndex(4)->waypoint()->position();
const auto crs = SGGeodesy::courseDeg(balenPos, murenPos);
// total distance should not change
fp1->insertWayptAtIndex(new Discontinuity(fp1), 4); // betwee BALEN and MUREN
CPPUNIT_ASSERT_DOUBLES_EQUAL(tdBefore, fp1->totalDistanceNm(), 1.0);
CPPUNIT_ASSERT_DOUBLES_EQUAL(fp1->legAtIndex(4)->courseDeg(),
SGGeodesy::courseDeg(balenPos, murenPos), 0.1);
CPPUNIT_ASSERT_DOUBLES_EQUAL(fp1->legAtIndex(4)->distanceNm(), 0.0, 0.1);
CPPUNIT_ASSERT_DOUBLES_EQUAL(fp1->legAtIndex(5)->courseDeg(),
SGGeodesy::courseDeg(balenPos, murenPos), 0.1);
CPPUNIT_ASSERT_DOUBLES_EQUAL(fp1->legAtIndex(5)->distanceNm(),
SGGeodesy::distanceNm(balenPos, murenPos), 0.1);
// ensure that pointAlongRoute works correctly going into the DISCON
const auto pos1 = fp1->pointAlongRoute(3, 20.0);
const auto validP1 = SGGeodesy::direct(balenPos, crs, 20.0 * SG_NM_TO_METER);
CPPUNIT_ASSERT(SGGeodesy::distanceM(pos1, validP1) < 500.0);
const auto pos2 = fp1->pointAlongRoute(5, -10.0);
const auto crs2 = SGGeodesy::courseDeg(murenPos, balenPos);
const auto validP2 = SGGeodesy::direct(murenPos, crs2, 10.0 * SG_NM_TO_METER);
CPPUNIT_ASSERT(SGGeodesy::distanceM(pos2, validP2) < 500.0);
// remove the discontinuity
fp1->deleteIndex(4);
CPPUNIT_ASSERT_DOUBLES_EQUAL(fp1->legAtIndex(4)->courseDeg(),
SGGeodesy::courseDeg(balenPos, murenPos), 0.1);
CPPUNIT_ASSERT_DOUBLES_EQUAL(fp1->legAtIndex(4)->distanceNm(),
SGGeodesy::distanceNm(balenPos, murenPos), 0.1);
CPPUNIT_ASSERT_DOUBLES_EQUAL(tdBefore, fp1->totalDistanceNm(), 1.0);
}
void FlightplanTests::testOnlyDiscontinuityRoute()
{
FlightPlanRef f = FlightPlan::create();
FGAirportRef depApt = FGAirport::getByIdent("LFBD"s);
f->setDeparture(depApt);
FGAirportRef destApt = FGAirport::getByIdent("EHAM"s);
f->setDestination(destApt);
f->insertWayptAtIndex(new Discontinuity(f), 0);
RoutePath rp1(f);
// discontinuity should act like a straight segment between preceeding and following
const double d = SGGeodesy::distanceNm(depApt->geod(), destApt->geod());
// CPPUNIT_ASSERT_DOUBLES_EQUAL(rp1.distanceForIndex(0), d, 0.5);
// start inserting waypoints ahead of the DISCON, Boeing FPL style
WayptRef wpt = f->waypointFromString("LMG"s);
f->insertWayptAtIndex(wpt, 0);
wpt = f->waypointFromString("KUKOR"s);
f->insertWayptAtIndex(wpt, 1);
wpt = f->waypointFromString("EPL"s);
f->insertWayptAtIndex(wpt, 2);
}
void FlightplanTests::testLeadingWPDynamic()
{
FlightPlanRef f = FlightPlan::create();
// plan has no departure, so this discon is floating
f->insertWayptAtIndex(new Discontinuity(f), 0);
auto ha = new HeadingToAltitude(f, "TO_3000"s, 90);
ha->setAltitude(3000, RESTRICT_AT);
f->insertWayptAtIndex(ha, 1);
RoutePath rp1(f);
// distance will be invalid, but shouldn;t assert or crash :)
CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, f->totalDistanceNm(), 0.001);
}
static double magVarFor(const SGGeod& geod)
{
double jd = globals->get_time_params()->getJD();
return sgGetMagVar(geod, jd) * SG_RADIANS_TO_DEGREES;
}
void FlightplanTests::testRadialIntercept()
{
// replicate AJO1R departure
FlightPlanRef f = makeTestFP("LFKC"s, "36"s, "LIRF"s, "25"s, "BUNAX BEBEV AJO"s);
// set BUNAX as overflight
f->legAtIndex(1)->waypoint()->setFlag(WPT_OVERFLIGHT);
f->insertWayptAtIndex(new BasicWaypt(SGGeod::fromDeg(8.78333, 42.566), "KC502"s, f), 1);
SGGeod pos = SGGeod::fromDeg(8.445556,42.216944);
auto intc = new RadialIntercept(f, "INTC"s, pos, 230, 5);
f->insertWayptAtIndex(intc, 3); // between BUNAX and BEBEV
RoutePath rtepath(f);
CPPUNIT_ASSERT_DOUBLES_EQUAL(230 + magVarFor(pos), f->legAtIndex(3)->courseDeg(), 1.0);
}
void FlightplanTests::loadFGFPWithoutDepartureArrival()
{
static_factory = std::make_shared();
FlightPlan::registerDelegateFactory(static_factory);
FlightPlanRef f = FlightPlan::create();
SGPath fgfpPath = simgear::Dir::current().path() / "test_fgfp_without_dep_arr.fgfp"s;
{
sg_ofstream s(fgfpPath);
s << R"(
2
EDDM
08R
EDDF
navaid
GIVMI
11.364700
48.701100
navaid
ERNAS
11.219400
48.844700
navaid
TALAL
11.085300
49.108300
navaid
ERMEL
11.044700
49.187800
navaid
PSA
9.348300
49.862200
)";
}
auto ourDelegate = TestFPDelegateFactory::delegateForPlan(f);
CPPUNIT_ASSERT(!ourDelegate->didLoad);
CPPUNIT_ASSERT(f->load(fgfpPath));
CPPUNIT_ASSERT(ourDelegate->didLoad);
CPPUNIT_ASSERT(ourDelegate->sawArrivalChange);
CPPUNIT_ASSERT(ourDelegate->sawDepartureChange);
}
void FlightplanTests::loadFGFPWithEmbeddedProcedures()
{
static_factory = std::make_shared();
FlightPlan::registerDelegateFactory(static_factory);
FlightPlanRef f = FlightPlan::create();
SGPath fgfpPath = simgear::Dir::current().path() / "test_fgfp_with_dep_arr.fgfp"s;
{
sg_ofstream s(fgfpPath);
s << R"(
2
EDDM
08R
EDDF
runway
true
08R
EDDM
navaid
GIVMI
11.364700
48.701100
navaid
ERNAS
11.219400
48.844700
navaid
TALAL
11.085300
49.108300
navaid
ERMEL
11.044700
49.187800
navaid
PSA
9.348300
49.862200
runway
true
36
EDDF
)";
}
auto ourDelegate = TestFPDelegateFactory::delegateForPlan(f);
CPPUNIT_ASSERT(!ourDelegate->didLoad);
CPPUNIT_ASSERT(f->load(fgfpPath));
CPPUNIT_ASSERT(ourDelegate->didLoad);
CPPUNIT_ASSERT(!ourDelegate->sawArrivalChange);
CPPUNIT_ASSERT(!ourDelegate->sawDepartureChange);
}
void FlightplanTests::loadFGFPWithOldProcedures()
{
if (!static_haveProcedures)
return;
FlightPlanRef f = FlightPlan::create();
SGPath fgfpPath = simgear::Dir::current().path() / "test_fgfp_old_procedure_idents.fgfp"s;
{
sg_ofstream s(fgfpPath);
s << R"(
2
KJFK
13L
CANDR
EHAM
BEDUM
runway
true
36
EDDF
)";
}
auto kjfk = FGAirport::findByIdent("KJFK"s);
auto eham = FGAirport::findByIdent("EHAM"s);
CPPUNIT_ASSERT(f->load(fgfpPath));
CPPUNIT_ASSERT(f->sid() == nullptr);
CPPUNIT_ASSERT(f->star() == nullptr);
}
void FlightplanTests::loadFGFPWithProcedureIdents()
{
if (!static_haveProcedures)
return;
FlightPlanRef f = FlightPlan::create();
SGPath fgfpPath = simgear::Dir::current().path() / "test_fgfp_procedure_idents.fgfp"s;
{
sg_ofstream s(fgfpPath);
s << R"(
2
KJFK
13L
DEEZZ5.13L
CANDR
EHAM
EEL1A
KUBAT
runway
true
36
EDDF
)";
}
auto kjfk = FGAirport::findByIdent("KJFK"s);
auto eham = FGAirport::findByIdent("EHAM"s);
CPPUNIT_ASSERT(f->load(fgfpPath));
CPPUNIT_ASSERT_EQUAL(f->sid()->ident(), "DEEZZ5.13L"s);
CPPUNIT_ASSERT_EQUAL(f->sidTransition()->ident(), "CANDR"s);
CPPUNIT_ASSERT_EQUAL(f->star()->ident(), "EEL1A"s);
CPPUNIT_ASSERT_EQUAL(f->starTransition()->ident(), "KUBAT"s);
}
void FlightplanTests::loadFGFPAsRoute()
{
static_factory = std::make_shared();
FlightPlan::registerDelegateFactory(static_factory);
FlightPlanRef f = FlightPlan::createRoute();
SGPath fgfpPath = simgear::Dir::current().path() / "test_fgfp_as_route.fgfp"s;
{
sg_ofstream s(fgfpPath);
s << R"(
2
true
EGLL
27R
EDDS
navaid
BIG
51.33087500
000.03481100
navaid
CLN
51.84841700
001.14761100
via
L620
1
BASNO
52.114586
2.487947
)";
}
auto ourDelegate = TestFPDelegateFactory::delegateForPlan(f);
CPPUNIT_ASSERT(!ourDelegate->didLoad);
CPPUNIT_ASSERT(f->load(fgfpPath));
CPPUNIT_ASSERT(f->isRoute());
CPPUNIT_ASSERT(ourDelegate->didLoad);
CPPUNIT_ASSERT(ourDelegate->sawArrivalChange);
CPPUNIT_ASSERT(ourDelegate->sawDepartureChange);
CPPUNIT_ASSERT_EQUAL(5, f->numLegs());
CPPUNIT_ASSERT_EQUAL("via"s, f->legAtIndex(3)->waypoint()->type());
auto actFP = f->clone("ACT", true);
CPPUNIT_ASSERT(!actFP->isRoute());
CPPUNIT_ASSERT(!actFP->isActive());
// check Via was expanded
CPPUNIT_ASSERT_EQUAL(8, actFP->numLegs());
}
void FlightplanTests::testCloningBasic()
{
FlightPlanRef fp1 = makeTestFP("EGCC"s, "23L"s, "EHAM"s, "24"s,
"TNT CLN"s);
fp1->setIdent("testplan"s);
fp1->setCruiseAltitudeFt(24000);
fp1->setCruiseSpeedKnots(448);
auto fp2 = fp1->clone("testplan2"s);
CPPUNIT_ASSERT(fp2->ident() == "testplan2"s);
CPPUNIT_ASSERT(fp2->departureAirport()->ident() == "EGCC"s);
CPPUNIT_ASSERT(fp2->departureRunway()->ident() == "23L"s);
CPPUNIT_ASSERT(fp2->destinationAirport()->ident() == "EHAM"s);
CPPUNIT_ASSERT(fp2->destinationRunway()->ident() == "24"s);
CPPUNIT_ASSERT_EQUAL(24000, fp2->cruiseAltitudeFt());
CPPUNIT_ASSERT_EQUAL(448, fp2->cruiseSpeedKnots());
CPPUNIT_ASSERT_EQUAL(5, fp2->numLegs());
CPPUNIT_ASSERT(fp2->legAtIndex(0)->waypoint()->source()->ident() == "23L"s);
CPPUNIT_ASSERT(fp2->legAtIndex(1)->waypoint()->source()->ident() == "TNT"s);
CPPUNIT_ASSERT(fp2->legAtIndex(2)->waypoint()->source()->ident() == "CLN"s);
}
void FlightplanTests::testCloningFGFP()
{
static_factory = std::make_shared();
FlightPlan::registerDelegateFactory(static_factory);
FlightPlanRef fp1 = FlightPlan::create();
SGPath fgfpPath = simgear::Dir::current().path() / "test_fgfp_cloning.fgfp"s;
{
sg_ofstream s(fgfpPath);
s << R"(
2
EDDM
08R
EDDF
navaid
GIVMI
11.364700
48.701100
navaid
ERNAS
11.219400
48.844700
navaid
TALAL
11.085300
49.108300
navaid
ERMEL
11.044700
49.187800
navaid
PSA
9.348300
49.862200
)";
}
auto ourDelegate = TestFPDelegateFactory::delegateForPlan(fp1);
CPPUNIT_ASSERT(!ourDelegate->didLoad);
CPPUNIT_ASSERT(fp1->load(fgfpPath));
CPPUNIT_ASSERT(ourDelegate->didLoad);
CPPUNIT_ASSERT(ourDelegate->sawArrivalChange);
CPPUNIT_ASSERT(ourDelegate->sawDepartureChange);
auto fp2 = fp1->clone();
auto secondDelegate = TestFPDelegateFactory::delegateForPlan(fp2);
CPPUNIT_ASSERT(secondDelegate->didLoad);
CPPUNIT_ASSERT(secondDelegate->sawWaypointsChange);
CPPUNIT_ASSERT(!secondDelegate->sawArrivalChange);
CPPUNIT_ASSERT(!secondDelegate->sawDepartureChange);
CPPUNIT_ASSERT(fp2->departureAirport()->ident() == "EDDM"s);
CPPUNIT_ASSERT(fp2->departureRunway()->ident() == "08R"s);
CPPUNIT_ASSERT(fp2->destinationAirport()->ident() == "EDDF"s);
CPPUNIT_ASSERT_EQUAL(fp2->legAtIndex(0)->waypoint()->source()->ident(), "08R"s);
CPPUNIT_ASSERT_EQUAL(fp2->legAtIndex(1)->waypoint()->source()->ident(), "GIVMI"s);
CPPUNIT_ASSERT_EQUAL(fp2->legAtIndex(5)->waypoint()->source()->ident(), "PSA"s);
CPPUNIT_ASSERT_EQUAL(fp2->legAtIndex(6)->waypoint()->source()->ident(), "EDDF"s);
CPPUNIT_ASSERT_EQUAL(7, fp2->numLegs());
}
void FlightplanTests::testCloningProcedures() {
// procedures not loaded, abandon test
if (!static_haveProcedures)
return;
static_factory = std::make_shared();
FlightPlan::registerDelegateFactory(static_factory);
auto egkk = FGAirport::findByIdent("EGKK"s);
auto sid = egkk->findSIDWithIdent("SAM3P"s);
FlightPlanRef fp1 = makeTestFP("EGKK"s, "08R"s, "EHAM"s, "18R"s,
""s);
//auto ourDelegate = TestFPDelegateFactory::delegateForPlan(fp1);
fp1->setSID(sid);
auto eham = FGAirport::findByIdent("EHAM"s);
auto eel1A = eham->findSTARWithIdent("EEL1A"s);
fp1->setSTAR(eel1A, "BEDUM"s);
auto fp2 = fp1->clone();
CPPUNIT_ASSERT(fp2->departureAirport()->ident() == "EGKK"s);
CPPUNIT_ASSERT(fp2->departureRunway()->ident() == "08R"s);
CPPUNIT_ASSERT(fp2->destinationAirport()->ident() == "EHAM"s);
CPPUNIT_ASSERT(fp2->destinationRunway()->ident() == "18R"s);
CPPUNIT_ASSERT(fp2->legAtIndex(0)->waypoint()->source()->ident() == "08R"s);
CPPUNIT_ASSERT_EQUAL(fp2->sid()->ident(), "SAM3P"s);
CPPUNIT_ASSERT_EQUAL(fp2->star()->ident(), "EEL1A"s);
CPPUNIT_ASSERT_EQUAL(fp2->starTransition()->ident(), "BEDUM"s);
}
void FlightplanTests::testBug2616()
{
auto edty = FGAirport::findByIdent("EDTY"s);
edty->testSuiteInjectProceduresXML(SGPath::fromUtf8(FG_TEST_SUITE_DATA) / "EDTY.procedures.xml");
auto ils28Approach = edty->findApproachWithIdent("ILS28"s);
CPPUNIT_ASSERT(ils28Approach);
FlightPlanRef fp1 = makeTestFP("EDDS"s, "25"s, "EDTY"s, "28"s,
""s);
fp1->setApproach(ils28Approach);
CPPUNIT_ASSERT(fp1->destinationRunway()->ident() == "28"s);
CPPUNIT_ASSERT(fp1->approach()->ident() == "ILS28"s);
fp1->activate();
CPPUNIT_ASSERT(fp1->isActive());
}
void FlightplanTests::testRoute()
{
FlightPlanRef fp1 = makeTestFP("EGKK"s, "08R"s, "EHAM"s, "18R"s,
"CLN VIA-L620/REDFA SPY"s, true);
CPPUNIT_ASSERT(fp1->isRoute());
CPPUNIT_ASSERT_EQUAL("via"s, fp1->legAtIndex(2)->waypoint()->type());
// activating a route should fail
fp1->activate();
CPPUNIT_ASSERT(!fp1->isActive());
// setting index should still work
fp1->setCurrentIndex(2);
CPPUNIT_ASSERT_EQUAL(2, fp1->currentIndex());
CPPUNIT_ASSERT(!fp1->isActive());
}