477 lines
14 KiB
C++
477 lines
14 KiB
C++
|
#include "config.h"
|
||
|
|
||
|
#include "test_suite/dataStore.hxx"
|
||
|
#include <ctime>
|
||
|
|
||
|
#include "TestDataLogger.hxx"
|
||
|
#include "testGlobals.hxx"
|
||
|
|
||
|
#include <simgear/io/iostreams/sgstream.hxx>
|
||
|
|
||
|
#if defined(HAVE_QT)
|
||
|
#include <GUI/QtLauncher.hxx>
|
||
|
#endif
|
||
|
|
||
|
#include <Main/globals.hxx>
|
||
|
#include <Main/options.hxx>
|
||
|
#include <Main/util.hxx>
|
||
|
#include <Main/FGInterpolator.hxx>
|
||
|
#include <Main/locale.hxx>
|
||
|
|
||
|
#include <Time/TimeManager.hxx>
|
||
|
|
||
|
#include <simgear/structure/event_mgr.hxx>
|
||
|
#include <simgear/timing/timestamp.hxx>
|
||
|
#include <simgear/timing/sg_time.hxx>
|
||
|
#include <simgear/math/sg_geodesy.hxx>
|
||
|
#include <simgear/props/props_io.hxx>
|
||
|
|
||
|
#include <Airports/airport.hxx>
|
||
|
#include <Navaids/FlightPlan.hxx>
|
||
|
#include <Navaids/waypoint.hxx>
|
||
|
#include <Navaids/routePath.hxx>
|
||
|
|
||
|
#include <Scripting/NasalSys.hxx>
|
||
|
|
||
|
using namespace flightgear;
|
||
|
|
||
|
namespace FGTestApi {
|
||
|
|
||
|
bool global_loggingToKML = false;
|
||
|
sg_ofstream global_kmlStream;
|
||
|
bool global_lineStringOpen = false;
|
||
|
|
||
|
namespace setUp {
|
||
|
|
||
|
void initTestGlobals(const std::string& testName)
|
||
|
{
|
||
|
assert(globals == nullptr);
|
||
|
globals = new FGGlobals;
|
||
|
|
||
|
DataStore &data = DataStore::get();
|
||
|
if (!data.getFGRoot().exists()) {
|
||
|
data.findFGRoot("");
|
||
|
}
|
||
|
globals->set_fg_root(data.getFGRoot());
|
||
|
|
||
|
// current dir
|
||
|
SGPath homePath = SGPath::fromUtf8(FGBUILDDIR) / "test_home";
|
||
|
if (!homePath.exists()) {
|
||
|
(homePath / "dummyFile").create_dir(0755);
|
||
|
}
|
||
|
|
||
|
globals->set_fg_home(homePath);
|
||
|
auto props = globals->get_props();
|
||
|
props->setStringValue("sim/fg-home", homePath.utf8Str());
|
||
|
|
||
|
// Activate headless mode.
|
||
|
globals->set_headless(true);
|
||
|
|
||
|
fgSetDefaults();
|
||
|
|
||
|
auto t = globals->add_new_subsystem<TimeManager>(SGSubsystemMgr::INIT);
|
||
|
t->bind();
|
||
|
t->init(); // establish mag-var data
|
||
|
|
||
|
/**
|
||
|
* Both the event manager and subsystem manager are initialised by the
|
||
|
* FGGlobals ctor, but only the subsystem manager is destroyed by the dtor.
|
||
|
* Here the event manager is added to the subsystem manager so it can be
|
||
|
* destroyed via the subsystem manager.
|
||
|
*/
|
||
|
globals->add_subsystem("events", globals->get_event_mgr(), SGSubsystemMgr::DISPLAY);
|
||
|
|
||
|
// necessary to avoid asserts: mark FGLocale as initialized
|
||
|
globals->get_locale()->selectLanguage({});
|
||
|
}
|
||
|
|
||
|
bool logPositionToKML(const std::string& testName)
|
||
|
{
|
||
|
// clear any previous state
|
||
|
if (global_loggingToKML) {
|
||
|
global_kmlStream.close();
|
||
|
global_lineStringOpen = false;
|
||
|
}
|
||
|
|
||
|
SGPath p = SGPath::desktop() / (testName + ".kml");
|
||
|
global_kmlStream.open(p);
|
||
|
if (!global_kmlStream.is_open()) {
|
||
|
SG_LOG(SG_GENERAL, SG_WARN, "unable to open:" << p);
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// pre-amble
|
||
|
global_kmlStream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||
|
"<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n"
|
||
|
"<Document>\n";
|
||
|
// need more precision for doubles when specifying lat/lon, see
|
||
|
// https://xkcd.com/2170/ :)
|
||
|
global_kmlStream.precision(12);
|
||
|
|
||
|
global_loggingToKML = true;
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
bool logLinestringsToKML(const std::string& testName)
|
||
|
{
|
||
|
// clear any previous state
|
||
|
if (global_loggingToKML) {
|
||
|
global_kmlStream.close();
|
||
|
global_lineStringOpen = false;
|
||
|
}
|
||
|
|
||
|
SGPath p = SGPath::desktop() / (testName + ".kml");
|
||
|
global_kmlStream.open(p);
|
||
|
if (!global_kmlStream.is_open()) {
|
||
|
SG_LOG(SG_GENERAL, SG_WARN, "unable to open:" << p);
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// pre-amble
|
||
|
global_kmlStream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||
|
"<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n"
|
||
|
"<Document>\n";
|
||
|
// need more precision for doubles when specifying lat/lon, see
|
||
|
// https://xkcd.com/2170/ :)
|
||
|
global_kmlStream.precision(12);
|
||
|
|
||
|
global_loggingToKML = false;
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
void initStandardNasal(bool withCanvas)
|
||
|
{
|
||
|
fgInitAllowedPaths();
|
||
|
|
||
|
auto nasalNode = globals->get_props()->getNode("nasal", true);
|
||
|
|
||
|
// load loadpriority.xml, for default modules load order
|
||
|
|
||
|
auto nasalLoadPriority = globals->get_props()->getNode("/sim/nasal-load-priority",true);
|
||
|
readProperties(globals->get_fg_root() / "Nasal/loadpriority.xml", nasalLoadPriority);
|
||
|
|
||
|
// set various props to reduce Nasal errors
|
||
|
auto props = globals->get_props();
|
||
|
props->setStringValue("sim/flight-model", "null");
|
||
|
props->setStringValue("sim/aircraft", "test-suite-aircraft");
|
||
|
|
||
|
props->setDoubleValue("sim/current-view/config/default-field-of-view-deg", 90.0);
|
||
|
// ensure /sim/view/config exists
|
||
|
props->setBoolValue("sim/view/config/foo", false);
|
||
|
|
||
|
props->setBoolValue("sim/rendering/precipitation-gui-enable", false);
|
||
|
props->setBoolValue("sim/rendering/precipitation-aircraft-enable", false);
|
||
|
|
||
|
// disable various larger modules
|
||
|
nasalNode->setBoolValue("canvas/enabled", withCanvas);
|
||
|
nasalNode->setBoolValue("jetways/enabled", false);
|
||
|
nasalNode->setBoolValue("jetways_edit/enabled", false);
|
||
|
nasalNode->setBoolValue("local_weather/enabled", false);
|
||
|
|
||
|
// Nasal needs the interpolator running
|
||
|
globals->add_subsystem("prop-interpolator", new FGInterpolator, SGSubsystemMgr::INIT);
|
||
|
|
||
|
// will be inited, since we already did that
|
||
|
globals->add_new_subsystem<FGNasalSys>(SGSubsystemMgr::INIT);
|
||
|
}
|
||
|
|
||
|
void populateFPWithoutNasal(flightgear::FlightPlanRef f,
|
||
|
const std::string& depICAO, const std::string& depRunway,
|
||
|
const std::string& destICAO, const std::string& destRunway,
|
||
|
const std::string& waypoints)
|
||
|
{
|
||
|
FGAirportRef depApt = FGAirport::getByIdent(depICAO);
|
||
|
f->setDeparture(depApt->getRunwayByIdent(depRunway));
|
||
|
|
||
|
|
||
|
FGAirportRef destApt = FGAirport::getByIdent(destICAO);
|
||
|
f->setDestination(destApt->getRunwayByIdent(destRunway));
|
||
|
|
||
|
// since we don't have the Nasal route-manager delegate, insert the
|
||
|
// runway waypoints manually
|
||
|
|
||
|
auto depRwy = new RunwayWaypt(f->departureRunway(), f);
|
||
|
depRwy->setFlag(WPT_DEPARTURE);
|
||
|
f->insertWayptAtIndex(depRwy, -1);
|
||
|
|
||
|
for (auto ws : simgear::strutils::split(waypoints)) {
|
||
|
WayptRef wpt = f->waypointFromString(ws);
|
||
|
if (!wpt) {
|
||
|
SG_LOG(SG_NAVAID, SG_ALERT, "No waypoint created for:" << ws);
|
||
|
continue;
|
||
|
}
|
||
|
f->insertWayptAtIndex(wpt, -1);
|
||
|
}
|
||
|
|
||
|
|
||
|
auto destRwy = f->destinationRunway();
|
||
|
f->insertWayptAtIndex(new BasicWaypt(destRwy->pointOnCenterline(-8 * SG_NM_TO_METER),
|
||
|
destRwy->ident() + "-8", f), -1);
|
||
|
f->insertWayptAtIndex(new RunwayWaypt(destRwy, f), -1);
|
||
|
}
|
||
|
|
||
|
void populateFPWithNasal(flightgear::FlightPlanRef f,
|
||
|
const std::string& depICAO, const std::string& depRunway,
|
||
|
const std::string& destICAO, const std::string& destRunway,
|
||
|
const std::string& waypoints)
|
||
|
{
|
||
|
FGAirportRef depApt = FGAirport::getByIdent(depICAO);
|
||
|
f->setDeparture(depApt->getRunwayByIdent(depRunway));
|
||
|
|
||
|
FGAirportRef destApt = FGAirport::getByIdent(destICAO);
|
||
|
f->setDestination(destApt->getRunwayByIdent(destRunway));
|
||
|
|
||
|
// insert after the last departure waypoint
|
||
|
int insertIndex = 1;
|
||
|
|
||
|
for (auto ws : simgear::strutils::split(waypoints)) {
|
||
|
WayptRef wpt = f->waypointFromString(ws);
|
||
|
f->insertWayptAtIndex(wpt, insertIndex++);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
} // End of namespace setUp.
|
||
|
|
||
|
void beginLineString(const std::string& ident)
|
||
|
{
|
||
|
global_lineStringOpen = true;
|
||
|
global_kmlStream << "<Placemark>\n";
|
||
|
if (!ident.empty()) {
|
||
|
global_kmlStream << "<name>" << ident << "</name>\n";
|
||
|
}
|
||
|
global_kmlStream << "<LineString>\n";
|
||
|
global_kmlStream << "<tessellate>1</tessellate>\n";
|
||
|
global_kmlStream << "<coordinates>\n";
|
||
|
}
|
||
|
|
||
|
void logCoordinate(const SGGeod& pos)
|
||
|
{
|
||
|
if (!global_lineStringOpen) {
|
||
|
beginLineString({});
|
||
|
}
|
||
|
|
||
|
global_kmlStream << pos.getLongitudeDeg() << "," << pos.getLatitudeDeg() << " " << endl;
|
||
|
}
|
||
|
|
||
|
void rawLogCoordinate(const SGGeod& pos)
|
||
|
{
|
||
|
global_kmlStream << pos.getLongitudeDeg() << "," << pos.getLatitudeDeg() << " " << endl;
|
||
|
}
|
||
|
|
||
|
void endCurrentLineString()
|
||
|
{
|
||
|
global_lineStringOpen = false;
|
||
|
global_kmlStream <<
|
||
|
"</coordinates>\n"
|
||
|
"</LineString>\n"
|
||
|
"</Placemark>\n" << endl;
|
||
|
}
|
||
|
|
||
|
void setPosition(const SGGeod& g)
|
||
|
{
|
||
|
if (global_loggingToKML) {
|
||
|
if (global_lineStringOpen) {
|
||
|
endCurrentLineString();
|
||
|
}
|
||
|
|
||
|
logCoordinate(g);
|
||
|
}
|
||
|
|
||
|
globals->get_props()->setDoubleValue("position/latitude-deg", g.getLatitudeDeg());
|
||
|
globals->get_props()->setDoubleValue("position/longitude-deg", g.getLongitudeDeg());
|
||
|
globals->get_props()->setDoubleValue("position/altitude-ft", g.getElevationFt());
|
||
|
}
|
||
|
|
||
|
const SGGeod getPosition()
|
||
|
{
|
||
|
return SGGeod::fromDegFt(
|
||
|
globals->get_props()->getDoubleValue("position/latitude-deg"),
|
||
|
globals->get_props()->getDoubleValue("position/longitude-deg"),
|
||
|
globals->get_props()->getDoubleValue("position/altitude-ft"));
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
void setPositionAndStabilise(const SGGeod& g)
|
||
|
{
|
||
|
setPosition(g);
|
||
|
for (int i=0; i<60; ++i) {
|
||
|
globals->get_subsystem_mgr()->update(0.015);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void runForTime(double t)
|
||
|
{
|
||
|
const int tickHz = 30;
|
||
|
const double tickDuration = 1.0 / tickHz;
|
||
|
|
||
|
int ticks = static_cast<int>(t * tickHz);
|
||
|
assert(ticks > 0);
|
||
|
|
||
|
const int logInterval = 0.5 * tickHz;
|
||
|
int nextLog = 0;
|
||
|
|
||
|
long startTime = globals->get_time_params()->get_cur_time();
|
||
|
|
||
|
for (int t = 0; t < ticks; ++t) {
|
||
|
globals->inc_sim_time_sec(tickDuration);
|
||
|
globals->get_time_params()->update(globals->get_view_position(), startTime, t * tickDuration);
|
||
|
globals->get_subsystem_mgr()->update(tickDuration);
|
||
|
|
||
|
if (nextLog == 0) {
|
||
|
if (global_loggingToKML) {
|
||
|
logCoordinate(globals->get_aircraft_position());
|
||
|
}
|
||
|
|
||
|
if (DataLogger::isActive()) {
|
||
|
DataLogger::instance()->writeRecord();
|
||
|
}
|
||
|
nextLog = logInterval;
|
||
|
} else {
|
||
|
nextLog--;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bool runForTimeWithCheck(double t, RunCheck check)
|
||
|
{
|
||
|
const int tickHz = 30;
|
||
|
const double tickDuration = 1.0 / tickHz;
|
||
|
|
||
|
int ticks = static_cast<int>(t * tickHz);
|
||
|
assert(ticks > 0);
|
||
|
const int logInterval = 0.5 * tickHz;
|
||
|
int nextLog = 0;
|
||
|
|
||
|
for (int t = 0; t < ticks; ++t) {
|
||
|
globals->inc_sim_time_sec(tickDuration);
|
||
|
globals->get_subsystem_mgr()->update(tickDuration);
|
||
|
|
||
|
if (nextLog == 0) {
|
||
|
if (global_loggingToKML) {
|
||
|
logCoordinate(globals->get_aircraft_position());
|
||
|
}
|
||
|
|
||
|
if (DataLogger::isActive()) {
|
||
|
DataLogger::instance()->writeRecord();
|
||
|
}
|
||
|
nextLog = logInterval;
|
||
|
} else {
|
||
|
nextLog--;
|
||
|
}
|
||
|
|
||
|
bool done = check();
|
||
|
if (done) {
|
||
|
if (global_loggingToKML) {
|
||
|
endCurrentLineString();
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
void adjustSimulationWorldTime(time_t desiredUnixTime)
|
||
|
{
|
||
|
int timeOffset = desiredUnixTime - time(nullptr);
|
||
|
globals->get_props()->setIntValue("/sim/time/cur-time-override", 0);
|
||
|
globals->get_props()->setIntValue("/sim/time/warp", timeOffset);
|
||
|
|
||
|
globals->get_subsystem<TimeManager>()->update(0.0);
|
||
|
}
|
||
|
|
||
|
void writeFlightPlanToKML(flightgear::FlightPlanRef fp)
|
||
|
{
|
||
|
if (!global_loggingToKML)
|
||
|
return;
|
||
|
|
||
|
RoutePath rpath(fp);
|
||
|
|
||
|
for (int i=0; i<fp->numLegs(); ++i) {
|
||
|
SGGeodVec legPath = rpath.pathForIndex(i);
|
||
|
auto wp = fp->legAtIndex(i)->waypoint();
|
||
|
|
||
|
writeGeodsToKML("FP-leg-" + wp->ident(), legPath);
|
||
|
|
||
|
SGGeod legWPPosition = wp->position();
|
||
|
writePointToKML("WP " + wp->ident(), legWPPosition);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void writeGeodsToKML(const std::string &label, const flightgear::SGGeodVec& geods)
|
||
|
{
|
||
|
if (global_lineStringOpen) {
|
||
|
endCurrentLineString();
|
||
|
}
|
||
|
|
||
|
beginLineString(label);
|
||
|
|
||
|
for (const auto& g : geods) {
|
||
|
logCoordinate(g);
|
||
|
}
|
||
|
|
||
|
endCurrentLineString();
|
||
|
}
|
||
|
|
||
|
void writePointToKML(const std::string& ident, const SGGeod& pos)
|
||
|
{
|
||
|
global_kmlStream << "<Placemark>\n";
|
||
|
global_kmlStream << "<name>" << ident << "</name>\n";
|
||
|
global_kmlStream << "<Point>\n";
|
||
|
global_kmlStream << "<coordinates>\n";
|
||
|
rawLogCoordinate(pos);
|
||
|
global_kmlStream << "</coordinates>\n";
|
||
|
global_kmlStream << "</Point>\n";
|
||
|
global_kmlStream << "</Placemark>\n";
|
||
|
}
|
||
|
|
||
|
bool executeNasal(const std::string& code)
|
||
|
{
|
||
|
auto nasal = globals->get_subsystem<FGNasalSys>();
|
||
|
if (!nasal) {
|
||
|
throw sg_exception("Nasal not available");
|
||
|
}
|
||
|
|
||
|
nasal->getAndClearErrorList();
|
||
|
std::string output, parseErrors;
|
||
|
bool ok = nasal->parseAndRunWithOutput(code, output, parseErrors);
|
||
|
if (!parseErrors.empty()) {
|
||
|
SG_LOG(SG_NASAL, SG_ALERT, "Errors running Nasal:" << parseErrors);
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return ok;
|
||
|
}
|
||
|
|
||
|
namespace tearDown {
|
||
|
|
||
|
void shutdownTestGlobals()
|
||
|
{
|
||
|
// The QApplication instance must be destroyed before exit() begins, see
|
||
|
// <https://bugreports.qt.io/browse/QTBUG-48709> (otherwise, segfault).
|
||
|
#if defined(HAVE_QT)
|
||
|
flightgear::shutdownQtApp();
|
||
|
#endif
|
||
|
|
||
|
delete globals;
|
||
|
globals = nullptr;
|
||
|
|
||
|
if (global_kmlStream) {
|
||
|
if (global_lineStringOpen) {
|
||
|
endCurrentLineString();
|
||
|
}
|
||
|
// post-amble
|
||
|
global_kmlStream << "</Document>\n"
|
||
|
"</kml>" << endl;
|
||
|
global_kmlStream.close();
|
||
|
global_loggingToKML = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
} // End of namespace tearDown.
|
||
|
|
||
|
} // End of namespace FGTestApi.
|