AP input: add property-path support to Inputs
Allow making the AP source indirect (via a string property), so that the actual source property can be configured or adjusted from a -set.xml, or at runtime.
This commit is contained in:
parent
3f2c9ece5a
commit
9a08d9fcd6
6 changed files with 239 additions and 15 deletions
|
@ -79,6 +79,22 @@ InputValue::InputValue( SGPropertyNode& prop_root,
|
|||
parse(prop_root, cfg, value, offset, scale);
|
||||
}
|
||||
|
||||
InputValue::~InputValue()
|
||||
{
|
||||
if (_pathNode) {
|
||||
_pathNode->removeChangeListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
void InputValue::initPropertyFromInitialValue()
|
||||
{
|
||||
double s = get_scale();
|
||||
if( s != 0 )
|
||||
_property->setDoubleValue( (_value - get_offset())/s );
|
||||
else
|
||||
_property->setDoubleValue(0); // if scale is zero, value*scale is zero
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
void InputValue::parse( SGPropertyNode& prop_root,
|
||||
SGPropertyNode& cfg,
|
||||
|
@ -128,6 +144,25 @@ void InputValue::parse( SGPropertyNode& prop_root,
|
|||
return;
|
||||
}
|
||||
|
||||
if ((n = cfg.getChild("property-path"))) {
|
||||
// cache the root node, in case of changes
|
||||
_rootNode = &prop_root;
|
||||
const auto trimmed = simgear::strutils::strip(n->getStringValue());
|
||||
_pathNode = prop_root.getNode(trimmed, true);
|
||||
_pathNode->addChangeListener(this);
|
||||
|
||||
// if <property> is defined, should we use it to initialise
|
||||
// the path prop? not doing so for now.
|
||||
|
||||
const auto path = simgear::strutils::strip(_pathNode->getStringValue());
|
||||
if (!path.empty()) {
|
||||
_property = _rootNode->getNode(path);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// if no <property> element, check for <prop> element for backwards
|
||||
// compatibility
|
||||
if( (n = cfg.getChild("property"))
|
||||
|
@ -138,19 +173,12 @@ void InputValue::parse( SGPropertyNode& prop_root,
|
|||
_property = prop_root.getNode(trimmed, true);
|
||||
if( valueNode )
|
||||
{
|
||||
// initialize property with given value
|
||||
// if both <prop> and <value> exist
|
||||
double s = get_scale();
|
||||
if( s != 0 )
|
||||
_property->setDoubleValue( (_value - get_offset())/s );
|
||||
else
|
||||
_property->setDoubleValue(0); // if scale is zero, value*scale is zero
|
||||
initPropertyFromInitialValue();
|
||||
}
|
||||
|
||||
return;
|
||||
} // of have a <property> or <prop>
|
||||
|
||||
|
||||
if( !valueNode )
|
||||
{
|
||||
// no <value>, <prop> or <expression> element, use text node
|
||||
|
@ -225,6 +253,21 @@ double InputValue::get_value() const
|
|||
return _abs ? fabs(value) : value;
|
||||
}
|
||||
|
||||
bool InputValue::is_enabled() const
|
||||
{
|
||||
if (_pathNode && !_property) {
|
||||
// if we have a configurable path, and it's currently not valid,
|
||||
// mark ourselves as disabled
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_condition) {
|
||||
return _condition->test();
|
||||
}
|
||||
|
||||
return true; // default to enab;ed
|
||||
}
|
||||
|
||||
void InputValue::collectDependentProperties(std::set<const SGPropertyNode*>& props) const
|
||||
{
|
||||
if (_property) props.insert(_property);
|
||||
|
@ -233,11 +276,35 @@ void InputValue::collectDependentProperties(std::set<const SGPropertyNode*>& pro
|
|||
if (_min) _min->collectDependentProperties(props);
|
||||
if (_max) _max->collectDependentProperties(props);
|
||||
if (_expression) _expression->collectDependentProperties(props);
|
||||
if (_pathNode) props.insert(_pathNode);
|
||||
}
|
||||
|
||||
void InputValue::valueChanged(SGPropertyNode *node)
|
||||
{
|
||||
assert(node == _pathNode);
|
||||
const auto path = simgear::strutils::strip(_pathNode->getStringValue());
|
||||
if (path.empty()) {
|
||||
// don't consider an empty string to mean the root node, that's not
|
||||
// useful behaviour
|
||||
_property.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
// important we don't create here: this allows an invalid path
|
||||
// to give us a null _property, which causes us to be marked as
|
||||
// disabled, allowing another input to be used
|
||||
auto propNode = _rootNode->getNode(path);
|
||||
if (propNode) {
|
||||
_property = propNode;
|
||||
} else {
|
||||
_property.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void InputValueList::collectDependentProperties(std::set<const SGPropertyNode*>& props) const
|
||||
{
|
||||
for (auto& iv: *this) {
|
||||
iv->collectDependentProperties(props);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ public:
|
|||
* and/or offset, clamped to min/max values, be periodical, bound to
|
||||
* conditions or evaluated from expressions.
|
||||
*/
|
||||
class InputValue : public SGReferenced {
|
||||
class InputValue : public SGReferenced, public SGPropertyChangeListener {
|
||||
private:
|
||||
double _value; // The value as a constant or initializer for the property
|
||||
bool _abs; // return absolute value
|
||||
|
@ -69,14 +69,19 @@ private:
|
|||
PeriodicalValue_ptr _periodical; //
|
||||
SGSharedPtr<const SGCondition> _condition;
|
||||
SGSharedPtr<SGExpressiond> _expression; ///< expression to generate the value
|
||||
|
||||
SGPropertyNode_ptr _pathNode;
|
||||
SGPropertyNode_ptr _rootNode;
|
||||
|
||||
void valueChanged(SGPropertyNode* node) override;
|
||||
void initPropertyFromInitialValue();
|
||||
public:
|
||||
InputValue( SGPropertyNode& prop_root,
|
||||
SGPropertyNode& node,
|
||||
double value = 0.0,
|
||||
double offset = 0.0,
|
||||
double scale = 1.0 );
|
||||
|
||||
~InputValue();
|
||||
|
||||
/**
|
||||
*
|
||||
* @param prop_root Root node for all properties with relative path
|
||||
|
@ -105,9 +110,7 @@ public:
|
|||
return _offset == NULL ? 0.0 : _offset->get_value();
|
||||
}
|
||||
|
||||
inline bool is_enabled() const {
|
||||
return _condition == NULL ? true : _condition->test();
|
||||
}
|
||||
bool is_enabled() const;
|
||||
|
||||
void collectDependentProperties(std::set<const SGPropertyNode*>& props) const;
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ set(TESTSUITE_SOURCES
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/testDigitalFilter.cxx
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/testPidController.cxx
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/testPidControllerData.cxx
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/testInputValue.cxx
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
|
@ -12,5 +13,6 @@ set(TESTSUITE_HEADERS
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/testDigitalFilter.hxx
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/testPidController.hxx
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/testPidControllerData.hxx
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/testInputValue.hxx
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
|
|
@ -19,8 +19,10 @@
|
|||
|
||||
#include "testDigitalFilter.hxx"
|
||||
#include "testPidController.hxx"
|
||||
|
||||
#include "testInputValue.hxx"
|
||||
|
||||
// Set up the unit tests.
|
||||
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(DigitalFilterTests, "Unit tests");
|
||||
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(PidControllerTests, "Unit tests");
|
||||
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(InputValueTests, "Unit tests");
|
||||
|
||||
|
|
115
test_suite/unit_tests/Autopilot/testInputValue.cxx
Normal file
115
test_suite/unit_tests/Autopilot/testInputValue.cxx
Normal file
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
SPDX-Copyright: James Turner
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "testInputValue.hxx"
|
||||
|
||||
#include <strstream>
|
||||
|
||||
#include "test_suite/FGTestApi/testGlobals.hxx"
|
||||
|
||||
|
||||
#include <Autopilot/autopilot.hxx>
|
||||
#include <Autopilot/inputvalue.hxx>
|
||||
#include <Main/fg_props.hxx>
|
||||
#include <Main/globals.hxx>
|
||||
|
||||
|
||||
#include <simgear/math/sg_random.hxx>
|
||||
#include <simgear/props/props_io.hxx>
|
||||
|
||||
using namespace FGXMLAutopilot;
|
||||
|
||||
// Set up function for each test.
|
||||
void InputValueTests::setUp()
|
||||
{
|
||||
FGTestApi::setUp::initTestGlobals("ap-inputvalue");
|
||||
}
|
||||
|
||||
|
||||
// Clean up after each test.
|
||||
void InputValueTests::tearDown()
|
||||
{
|
||||
FGTestApi::tearDown::shutdownTestGlobals();
|
||||
}
|
||||
|
||||
|
||||
SGPropertyNode_ptr InputValueTests::configFromString(const std::string& s)
|
||||
{
|
||||
SGPropertyNode_ptr config = new SGPropertyNode;
|
||||
|
||||
std::istringstream iss(s);
|
||||
readProperties(iss, config);
|
||||
return config;
|
||||
}
|
||||
|
||||
void InputValueTests::testPropertyPath()
|
||||
{
|
||||
|
||||
sg_srandom(999);
|
||||
|
||||
auto config = configFromString(R"(<?xml version="1.0" encoding="UTF-8"?>
|
||||
<PropertyList>
|
||||
<property-path>/test/altitude-ft-node-path</property-path>
|
||||
<value>1.23</value>
|
||||
</PropertyList>
|
||||
)");
|
||||
|
||||
fgSetString("/test/altitude-ft-node-path", "/test/a");
|
||||
fgSetDouble("/test/a", 0.5);
|
||||
|
||||
InputValue_ptr valueA = new InputValue(*globals->get_props(), *config);
|
||||
|
||||
CPPUNIT_ASSERT(valueA->is_enabled());
|
||||
// check value is not written back
|
||||
CPPUNIT_ASSERT_DOUBLES_EQUAL(0.5, valueA->get_value(), 0.001);
|
||||
CPPUNIT_ASSERT_DOUBLES_EQUAL(0.5, fgGetDouble("/test/a"), 0.001);
|
||||
|
||||
fgSetDouble("/test/a", 2.34);
|
||||
CPPUNIT_ASSERT_DOUBLES_EQUAL(2.34, valueA->get_value(), 0.001);
|
||||
|
||||
fgSetString("/test/altitude-ft-node-path", "blah");
|
||||
CPPUNIT_ASSERT(!valueA->is_enabled());
|
||||
// <value> is used
|
||||
CPPUNIT_ASSERT_DOUBLES_EQUAL(1.23, valueA->get_value(), 0.001);
|
||||
|
||||
|
||||
fgSetDouble("/foo/bpath", 99);
|
||||
fgSetString("/test/altitude-ft-node-path", "/foo/bpath");
|
||||
CPPUNIT_ASSERT(valueA->is_enabled());
|
||||
CPPUNIT_ASSERT_DOUBLES_EQUAL(99.0, valueA->get_value(), 0.001);
|
||||
|
||||
fgSetDouble("/foo/bpath", -45.1);
|
||||
CPPUNIT_ASSERT_DOUBLES_EQUAL(-45.1, valueA->get_value(), 0.001);
|
||||
|
||||
// start with different config
|
||||
auto config2 = configFromString(R"(<?xml version="1.0" encoding="UTF-8"?>
|
||||
<PropertyList>
|
||||
<property-path>/test/indicated-knots-node-path</property-path>
|
||||
</PropertyList>
|
||||
)");
|
||||
|
||||
InputValue_ptr valueB = new InputValue(*globals->get_props(), *config2);
|
||||
CPPUNIT_ASSERT(!valueB->is_enabled());
|
||||
CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, valueB->get_value(), 0.001);
|
||||
|
||||
fgSetString("/test/indicated-knots-node-path", "/instruments/airspeed/output/knots");
|
||||
CPPUNIT_ASSERT(!valueB->is_enabled());
|
||||
CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, valueB->get_value(), 0.001);
|
||||
|
||||
// create the property, but this does not trigger the change listener, so
|
||||
// stays invalid
|
||||
fgSetDouble("/instruments/airspeed/output/knots", 415);
|
||||
CPPUNIT_ASSERT(!valueB->is_enabled());
|
||||
CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, valueB->get_value(), 0.001);
|
||||
|
||||
// set the path again (with some whitespace, which is trimmed)
|
||||
fgSetString("/test/indicated-knots-node-path", " /instruments/airspeed/output/knots ");
|
||||
CPPUNIT_ASSERT(valueB->is_enabled());
|
||||
CPPUNIT_ASSERT_DOUBLES_EQUAL(415.0, valueB->get_value(), 0.001);
|
||||
|
||||
fgSetString("/test/indicated-knots-node-path", "");
|
||||
CPPUNIT_ASSERT(!valueB->is_enabled());
|
||||
|
||||
}
|
35
test_suite/unit_tests/Autopilot/testInputValue.hxx
Normal file
35
test_suite/unit_tests/Autopilot/testInputValue.hxx
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
SPDX-Copyright: James Turner
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cppunit/TestFixture.h>
|
||||
#include <cppunit/extensions/HelperMacros.h>
|
||||
|
||||
#include <simgear/props/props.hxx>
|
||||
|
||||
|
||||
// The system tests.
|
||||
class InputValueTests : public CppUnit::TestFixture
|
||||
{
|
||||
// Set up the test suite.
|
||||
CPPUNIT_TEST_SUITE(InputValueTests);
|
||||
CPPUNIT_TEST(testPropertyPath);
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
SGPropertyNode_ptr configFromString(const std::string& s);
|
||||
|
||||
public:
|
||||
// Set up function for each test.
|
||||
void setUp();
|
||||
|
||||
// Clean up after each test.
|
||||
void tearDown();
|
||||
|
||||
|
||||
// The tests.
|
||||
void testPropertyPath();
|
||||
};
|
Loading…
Add table
Reference in a new issue