1
0
Fork 0
fgdata/Nasal/props.nas

726 lines
26 KiB
Text
Raw Normal View History

2003-12-01 14:36:22 +00:00
##
# Node class definition. The class methods simply wrap the
# low level extension functions which work on a "ghost" handle to a
2003-12-01 14:36:22 +00:00
# SGPropertyNode object stored in the _g field.
#
# Not all of the features of SGPropertyNode are supported. There is
# no support for ties, obviously, as that wouldn't make much sense
# from a Nasal context. The various get/set methods work only on the
# local node, there is no equivalent of the "relative path" variants
# available in C++; just use node.getNode(path).whatever() instead.
#
var Node = {
getNode : func wrap(_getNode(me._g, arg)),
getParent : func wrap(_getParent(me._g, arg)),
getChild : func wrap(_getChild(me._g, arg)),
getChildren : func wrap(_getChildren(me._g, arg)),
setChildren : func wrap(_setChildren(me._g, arg)),
addChild : func wrap(_addChild(me._g, arg)),
addChildren : func wrap(_addChildren(me._g, arg)),
removeChild : func wrap(_removeChild(me._g, arg)),
removeChildren : func wrap(_removeChildren(me._g, arg)),
removeAllChildren: func wrap(_removeAllChildren(me._g, arg)),
getAliasTarget : func wrap(_getAliasTarget(me._g, arg)),
2008-11-12 14:43:41 +00:00
getName : func _getName(me._g, arg),
getIndex : func _getIndex(me._g, arg),
getType : func _getType(me._g, arg),
isNumeric : func _isNumeric(me._g, arg),
isInt : func _isInt(me._g, arg),
getAttribute : func _getAttribute(me._g, arg),
setAttribute : func _setAttribute(me._g, arg),
getValue : func _getValue(me._g, arg),
setValue : func _setValue(me._g, arg),
setValues : func _setValues(me._g, arg),
setIntValue : func _setIntValue(me._g, arg),
setBoolValue : func _setBoolValue(me._g, arg),
2020-04-19 17:57:16 +00:00
toggleBoolValue: func _toggleBoolValue(me._g, arg),
setDoubleValue : func _setDoubleValue(me._g, arg),
2020-04-15 14:04:24 +00:00
# adjustValue works like setDoubleValue but adds the argument to the
# current value of the node
adjustValue : func _adjustValue(me._g, arg),
unalias : func _unalias(me._g, arg),
alias : func(n) _alias(me._g, [isa(n, Node) ? n._g : n]),
equals : func(n) _equals(me._g, [isa(n, Node) ? n._g : n]),
clearValue : func _alias(me._g, [_globals()]) and me.unalias(),
2003-12-01 14:36:22 +00:00
getPath : func {
var (name, index, parent) = (me.getName(), me.getIndex(), me.getParent());
if(index != 0) { name ~= "[" ~ index ~ "]"; }
if(parent != nil) { name = parent.getPath() ~ "/" ~ name; }
return name;
},
getBoolValue : func {
var val = me.getValue();
var mytype = me.getType();
if((mytype == "STRING" or mytype == "UNSPECIFIED") and val == "false") return 0;
return !!val;
},
getIntValue : func {
return math.round(me.getValue());
},
getDoubleValue : func {
return num(me.getValue());
},
remove : func {
if((var p = me.getParent()) == nil) return nil;
p.removeChild(me.getName(), me.getIndex());
},
2019-02-12 17:35:10 +00:00
# follow alias links to "real" node (e.g. where the data is stored)
# optional argument: property node or prop path to resolve
# if no argument is given, operate on current obj ("me")
resolveAlias : func(p = nil) {
if (p == nil)
p = me;
elsif (isscalar(p))
2019-02-12 17:35:10 +00:00
p = globals.getNode(p);
if (isa(p, Node)) {
while (p.getAttribute("alias")) {
p = p.getAliasTarget();
}
}
return p;
},
# add n to int property, do nothing, if prop type is not int
# n will be integer and defaults to 1
increment: func(n = 1) {
if (me.isInt()) {
var v = me.getValue() + int(n);
if (me.setIntValue(v)) return v;
}
else {
2020-04-24 13:55:11 +00:00
logprint(DEV_ALERT, "props.increment() can be used only on integer props "~
me.getPath()~" "~me.getType());
return nil;
}
},
# sub n from int property, do nothing, if prop type is not int
# n will be integer and defaults to 1
decrement: func(n = 1) {
if (me.isInt())
var v = me.getValue() - int(n);
if (me.setIntValue(v)) return v;
else {
2020-04-24 13:55:11 +00:00
logprint(DEV_ALERT, "props.decrement() can be used only on integer props"~
me.getPath()~" "~me.getType());
return 0;
}
},
# checks if a string can be used as a prop name
# returns 0 (invalid) or 1 (valid)
# as string operations are expensive, use this only when necessary,
# especially do not use in update loops
isValidPropName: func(s) {
if (!size(s)) return 0;
var _ispropspecial = func(c) { c == `_` or c == `.` or c == `-`; }
var _ispropname = func(c) string.isalnum(c) or _ispropspecial(c);
if (!string.isalpha(s[0]) and s[0] != `_`) return 0;
for (var i=1; i < size(s); i += 1) {
if (!_ispropname(s[i])) return 0;
}
return 1;
},
# replaces any invalid char by "_"
# as string operations are expensive, use this only when necessary,
# especially do not use in update loops
makeValidPropName: func(s) {
if (!size(s)) return nil;
var _ispropspecial = func(c) { c == `_` or c == `.` or c == `-`; }
var _ispropname = func(c) string.isalnum(c) or _ispropspecial(c);
var rv = "";
rv ~= (!string.isalpha(s[0])) ? "_" : chr(s[0]);
for (var i=1; i < size(s); i += 1) {
rv ~= (_ispropname(s[i])) ? chr(s[i]) : "_";
}
return rv;
},
2003-12-01 14:36:22 +00:00
};
##
# Static constructor for a Node object. Accepts a Nasal hash
# expression to initialize the object a-la setValues().
#
Node.new = func(values = nil) {
var result = wrapNode(_new());
if(ishash(values))
result.setValues(values);
return result;
}
##
# Counter piece of setValues(). Returns a hash with all values
# in the subtree. Nodes with same name are returned as vector,
# where the original node indices are lost. The function should
# only be used if all or almost all values are needed, and never
# in performance-critical code paths. If it's called on a node
# without children, then the result is equivalent to getValue().
#
Node.getValues = func {
var children = me.getChildren();
if(!size(children)) return me.getValue();
var val = {};
var numchld = {};
foreach(var c; children) {
var name = c.getName();
if(contains(numchld, name)) { var nc = numchld[name]; }
else {
var nc = size(me.getChildren(name));
numchld[name] = nc;
if(nc > 1 and !contains(val, name)) val[name] = [];
}
if(nc > 1) append(val[name], c.getValues());
else val[name] = c.getValues();
}
return val;
}
##
# Initializes property if it's still undefined. First argument
# is a property name/path. It can also be nil or an empty string,
# in which case the node itself gets initialized, rather than one
# of its children. Second argument is the default value. The third,
# optional argument is a property type (one of "STRING", "DOUBLE",
# "INT", or "BOOL"). If it is omitted, then "DOUBLE" is used for
# numbers, and STRING for everything else. Returns the property
# as props.Node. The fourth optional argument enforces a type if
# non-zero.
#
Node.initNode = func(path = nil, value = 0, type = nil, force = 0) {
var prop = me.getNode(path or "", 1);
if(prop.getType() != "NONE") value = prop.getValue();
if(force) prop.clearValue();
if(type == nil) prop.setValue(value);
elsif(type == "DOUBLE") prop.setDoubleValue(value);
elsif(type == "INT") prop.setIntValue(value);
elsif(type == "BOOL") prop.setBoolValue(value);
elsif(type == "STRING") prop.setValue("" ~ value);
else die("initNode(): unsupported type '" ~ type ~ "'");
return prop;
}
2003-12-01 14:36:22 +00:00
##
# Useful debugging utility. Recursively dumps the full state of a
# Node object to the console. Try binding "props.dump(props.globals)"
# to a key for a fun hack.
#
var dump = func {
2003-12-01 14:36:22 +00:00
if(size(arg) == 1) { prefix = ""; node = arg[0]; }
else { prefix = arg[0]; node = arg[1]; }
index = node.getIndex();
type = node.getType();
name = node.getName();
val = node.getValue();
if(val == nil) { val = "nil"; }
name = prefix ~ name;
if(index > 0) { name = name ~ "[" ~ index ~ "]"; }
print(name, " {", type, "} = ", val);
# Don't recurse into aliases, lest we get stuck in a loop
if(type != "ALIAS") {
children = node.getChildren();
foreach(c; children) { dump(name ~ "/", c); }
}
}
##
# Recursively copy property branch from source Node to
# destination Node. Doesn't copy aliases. Copies attributes
# if optional third argument is set and non-zero.
#
var copy = func(src, dest, attr = 0) {
var sp = src.getPath() or "";
var dp = dest.getPath() or "";
# sp and dp may be equal but on different trees!
# check if dest is sub node of source
sp = split("/", sp);
dp = split("/", dp);
while (size(sp) and size(dp) and (sp[0] == dp[0])) {
sp = subvec(sp, 1);
dp = subvec(dp, 1);
}
foreach(var c; src.getChildren()) {
var name = c.getName();
var i = c.getIndex();
if (i) name ~= "["~i~"]";
if (!(!size(sp) and size(dp) and name == dp[0]))
copy(src.getNode(name), dest.getNode(name, 1), attr);
else {
logprint(DEV_WARN, "props.copy() skipping "~name~" (recursion!)");
}
}
var type = src.getType();
var val = src.getValue();
if(type == "ALIAS" or type == "NONE") return;
elsif(type == "BOOL") dest.setBoolValue(val);
elsif(type == "INT" or type == "LONG") dest.setIntValue(val);
elsif(type == "FLOAT" or type == "DOUBLE") dest.setDoubleValue(val);
else dest.setValue(val);
if(attr) dest.setAttribute(src.getAttribute());
}
2003-12-01 14:36:22 +00:00
##
# Utility. Turns any ghosts it finds (either solo, or in an
# array) into Node objects.
#
var wrap = func(node) {
if(isghost(node)) {
return wrapNode(node);
} elsif(isvec(node)) {
var v = node;
var n = size(v);
for(var i=0; i<n; i+=1) { v[i] = wrapNode(v[i]); }
2003-12-01 14:36:22 +00:00
return v;
}
return node;
2003-12-01 14:36:22 +00:00
}
##
# Utility. Returns a new object with its superclass/parent set to the
# Node object and its _g (ghost) field set to the specified object.
# Nasal's literal syntax can be pleasingly terse. I like that. :)
#
var wrapNode = func(node) { { parents : [Node], _g : node } }
2003-12-01 14:36:22 +00:00
##
# Global property tree. Set once at initialization. Is that OK?
# Does anything ever call globals.set_props() from C++? May need to
# turn this into a function if so.
#
var globals = wrapNode(_globals());
2003-12-01 14:36:22 +00:00
2014-05-31 02:45:05 +00:00
##
# Shortcut for props.globals.getNode().
#
var getNode = func return call(props.globals.getNode, arg, props.globals);
##
# Sets all indexed property children to a single value. arg[0]
# specifies a property name (e.g. /controls/engines/engine), arg[1] a
# path under each node of that name to set (e.g. "throttle"), arg[2]
# is the value.
#
var setAll = func(base, child, value) {
var node = props.globals.getNode(base);
if(node == nil) return;
var name = node.getName();
node = node.getParent();
if(node == nil) return;
var children = node.getChildren();
foreach(var c; children)
if(c.getName() == name)
c.getNode(child, 1).setValue(value);
}
# createNodeObjectsFromHash - create nasal node objects from hash
# property_list: hash; where keys are variable names and values are
# property paths
# { foo: "/some/prop/foo", bar: "/some/other/prop", }
# namespace: optional; variables (objects) are created in this namespace
# defaults to namespace of caller, e.g. after calling this
# you can use foo.getValue() or bar.addChild()
#
var createNodeObjectsFromHash = func (property_list, namespace = nil) {
if (namespace == nil) {
namespace = caller(1)[0];
}
if (!ishash(namespace)) {
2020-04-20 14:24:49 +00:00
logprint(LOG_WARN, "createNodeObjectsFromHash: Error, namespace argument is not a hash.");
return nil;
}
if (!ishash(property_list)) {
2020-04-20 14:24:49 +00:00
logprint(LOG_WARN, "createNodeObjectsFromHash: Error, property_list argument is not a hash.");
return nil;
}
foreach (key; keys(property_list)) {
namespace[key] = props.getNode(property_list[key],1);
}
}
##
# Turns about anything into a list of props.Nodes, including ghosts,
# path strings, vectors or hashes containing, as well as functions
# returning any of the former and in arbitrary nesting. This is meant
# to be used in functions whose main purpose is to handle collections
# of properties.
#
var nodeList = func {
var list = [];
foreach(var a; arg) {
if(isa(a, Node))
append(list, a);
elsif(isscalar(a))
append(list, props.globals.getNode(a, 1));
elsif(isvec(a))
foreach(var i; a)
list ~= nodeList(i);
elsif(ishash(a))
foreach(var i; keys(a))
list ~= nodeList(a[i]);
elsif(isfunc(a))
list ~= nodeList(a());
elsif(isghost(a) and ghosttype(a) == "prop")
append(list, wrapNode(a));
else
die("nodeList: invalid nil property");
}
return list;
}
##
# Compiles a <condition> property branch according to the rules
# set out in $FG_ROOT/Docs/README.conditions into a Condition object.
# The 'test' method of the returend object can be used to evaluate
# the condition.
# The function returns nil on error.
#
var compileCondition = func(p) {
if(p == nil) return nil;
if(!isa(p, Node)) p = props.globals.getNode(p);
return _createCondition(p._g);
}
##
# Evaluates a <condition> property branch according to the rules
2007-03-20 16:23:23 +00:00
# set out in $FG_ROOT/Docs/README.conditions. Undefined conditions
# and a nil argument are "true". The function dumps the condition
# branch and returns nil on error.
#
var condition = func(p) {
if(p == nil) return 1;
if(!isa(p, Node)) p = props.globals.getNode(p);
return _cond_and(p)
}
var _cond_and = func(p) {
foreach(var c; p.getChildren())
if(!_cond(c)) return 0;
return 1;
}
var _cond_or = func(p) {
foreach(var c; p.getChildren())
if(_cond(c)) return 1;
return 0;
}
var _cond = func(p) {
var n = p.getName();
if(n == "or") return _cond_or(p);
if(n == "and") return _cond_and(p);
if(n == "not") return !_cond_and(p);
if(n == "equals") return _cond_cmp(p, 0);
if(n == "not-equals") return !_cond_cmp(p, 0);
if(n == "less-than") return _cond_cmp(p, -1);
if(n == "greater-than") return _cond_cmp(p, 1);
if(n == "less-than-equals") return !_cond_cmp(p, 1);
if(n == "greater-than-equals") return !_cond_cmp(p, -1);
if(n == "property") return !!getprop(p.getValue());
2020-04-20 14:24:49 +00:00
logprint(LOG_ALERT, "condition: invalid operator ", n);
dump(p);
return nil;
}
var _cond_cmp = func(p, op) {
var left = p.getChild("property", 0, 0);
if(left != nil) { left = getprop(left.getValue()); }
else {
2020-04-20 14:24:49 +00:00
logprint(LOG_ALERT, "condition: no left value");
dump(p);
return nil;
}
var right = p.getChild("property", 1, 0);
if(right != nil) { right = getprop(right.getValue()); }
else {
right = p.getChild("value", 0, 0);
if(right != nil) { right = right.getValue(); }
else {
2020-04-20 14:24:49 +00:00
logprint(LOG_ALERT, "condition: no right value");
dump(p);
return nil;
}
}
if (left == nil) left = 0.0;
if (right == nil) right = 0.0;
if(op < 0) return left < right;
if(op > 0) return left > right;
return left == right;
}
##
# Runs <binding> as described in $FG_ROOT/Docs/README.commands using
# a given module by default, and returns 1 if fgcommand() succeeded,
# or 0 otherwise. The module name won't override a <module> defined
# in the binding.
#
var runBinding = func(node, module = nil) {
if(module != nil and node.getNode("module") == nil)
node.getNode("module", 1).setValue(module);
var cmd = node.getNode("command", 1).getValue() or "null";
condition(node.getNode("condition")) ? fgcommand(cmd, node) : 0;
}
#---------------------------------------------------------------------------
# Property / object update manager
#
# - Manage updates when a value has changed more than a predetermined amount.
# This class is designed to make updating displays (e.g. canvas), or
# performing actions based on a property (or value in a hash) changing
# by more than the preset amount.
# This can make a significant improvement to performance compared to simply
# redrawing a canvas in an update loop.
# - Author : Richard Harrison (rjh@zaretto.com)
#---------------------------------------------------------------------------*/
#example usage:
# this is using the hashlist (which works well with an Emesary notification)
# basically when the method is called it will call each section (in the lambda)
# when the value changes by more than the amount specified as the second parameter.
# It is possible to reference multiple elements from the hashlist in each FromHashList; if either
# one changes then it will result in the lambda being called.
#
# obj.update_items = [
# UpdateManager.FromHashList(["VV_x","VV_y"], 0.01, func(val)
# {
# obj.VV.setTranslation (val.VV_x, val.VV_y + pitch_offset);
# }),
# UpdateManager.FromHashList(["pitch","roll"], 0.025, func(hdp)
# {
# obj.ladder.setTranslation (0.0, hdp.pitch * pitch_factor+pitch_offset);
# obj.ladder.setCenter (118,830 - hdp.pitch * pitch_factor-pitch_offset);
# obj.ladder.setRotation (-hdp.roll_rad);
# obj.roll_pointer.setRotation (hdp.roll_rad);
# }),
# props.UpdateManager.FromProperty("velocities/airspeed-kt", 0.01, func(val)
# {
# obj.ias_range.setTranslation(0, val * ias_range_factor);
# }),
# props.UpdateManager.FromPropertyHashList(["orientation/alpha-indicated-deg", "orientation/side-slip-deg"], 0.1, func(val)
# {
# obj.VV_x = val.property["orientation/side-slip-deg"].getValue()*10; # adjust for view
# obj.VV_y = val.property["orientation/alpha-indicated-deg"].getValue()*10; # adjust for view
# obj.VV.setTranslation (obj.VV_x, obj.VV_y);
# }),
# ]
#
#==== the update loop then becomes ======
#
# foreach(var update_item; me.update_items)
# {
# # hdp is a data provider that can be used as the hashlist for the property
# # update from hash methods.
# update_item.update(hdp);
# }
#
var UpdateManager =
{
_updateProperty : func(_property)
{
},
#
# Monitor a property for a change more than the delta.
# - type of the property is used to determine if a delta change
# can be detected or whether to fire on every value change (strings)
FromProperty : func(_propname, _delta, _changed_method)
{
var obj = {parents : [UpdateManager] };
obj.propname = _propname;
obj.property = props.globals.getNode(_propname);
obj.delta = _delta;
obj.curval = obj.property.getValue();
obj.lastval = obj.curval;
obj.changed = _changed_method;
obj.update = func(obj)
{
me.curval = me.property.getValue();
if (me.curval != nil)
{
me.localType = me.property.getType();
if (me.localType == "INT" or me.localType == "LONG" or me.localType == "FLOAT" or me.localType == "DOUBLE")
{
if(me.lastval == nil or math.abs(me.lastval - me.curval) >= me.delta)
{
me.lastval = me.curval;
me.changed(me.curval);
}
}
else if(me.lastval == nil or me.lastval != me.curval)
{
me.lastval = me.curval;
me.changed(me.curval);
}
}
};
obj.update(obj);
return obj;
},
#
# Determine if specifc property is a numeric (false usually means a string)
IsNumeric : func(hashkey)
{
me.localType = me.property[hashkey].getType();
if (me.localType == "UNSPECIFIED") {
print("UpdateManager: warning ",hashkey," is ",ty, " excluding from update");
me.property[hashkey] = nil;
}
if (me.localType == "INT" or me.localType == "LONG" or me.localType == "FLOAT" or me.localType == "DOUBLE")
return 1;
else
return 0;
},
#
# Monitor list of properties for a change more than the delta.
# - type of the property is used to determine if a delta change
# can be detected or whether to fire on every value change (strings)
FromPropertyHashList : func(_keylist, _delta, _changed_method)
{
var obj = {parents : [UpdateManager] };
obj.hashkeylist = _keylist;
obj.delta = _delta;
obj.lastval = {};
obj.hashkey = nil;
obj.changed = _changed_method;
obj.needs_update = 0;
obj.property = {};
obj.is_numeric = {};
foreach (hashkey; obj.hashkeylist) {
obj.property[hashkey] = props.globals.getNode(hashkey);
obj.lastval[hashkey] = nil;
# var ty = obj.property[hashkey].getType();
# if (ty == "INT" or ty == "LONG" or ty == "FLOAT" or ty == "DOUBLE") {
# obj.is_numeric[hashkey] = 1;
# } else
# obj.is_numeric[hashkey] = 0;
#print("create: ", hashkey," ", ty, " isnum=",obj.is_numeric[hashkey]);
# if (ty == "UNSPECIFIED")
# print("UpdateManager: warning ",hashkey," is ",ty);
}
obj.update = func(obj)
{
if (me.lastval == nil)
me.needs_update = 1;
else {
me.needs_update = 0;
foreach (hashkey; me.hashkeylist) {
if (me.property[hashkey] != nil) {
me.valIsNumeric = me.IsNumeric(hashkey);
if (me.lastval[hashkey] == nil
or (me.valIsNumeric and (math.abs(me.lastval[hashkey] - me.property[hashkey].getValue()) >= me.delta))
or (!me.valIsNumeric and (me.lastval[hashkey] != me.property[hashkey].getValue()))) {
me.needs_update = 1;
break;
}
}
}
}
if (me.needs_update) {
me.changed(me);
foreach (hashkey; me.hashkeylist) {
me.lastval[hashkey] = me.property[hashkey].getValue();
}
}
}
;
return obj;
},
# Monitor an individual hash value for a change more than the delta.
# - when the hash value is a string use nil as the delta to indicate that
# a simple value changed comparison is required.
# - can also use nil to detect any change; e.g. in
FromHashValue : func(_key, _delta, _changed_method)
{
var obj = {parents : [UpdateManager] };
obj.hashkey = _key;
obj.delta = _delta;
obj.isnum = _delta != nil;
obj.curval = nil;
obj.lastval = nil;
obj.changed = _changed_method;
obj.update = func(obj)
{
me.curval = obj[me.hashkey];
if (me.curval != nil) {
if (me.isnum) {
me.curval = num(me.curval);
if (me.lastval == nil or math.abs(me.lastval - me.curval) >= me.delta) {
me.lastval = me.curval;
me.changed(me.curval);
}
} else {
if (me.lastval == nil or me.lastval != me.curval) {
me.lastval = me.curval;
me.changed(me.curval);
}
}
}
}
;
return obj;
},
#
# Monitor a list of hash values for a change more than the delta.
# - when the hash value is a string use nil as the delta to indicate that
# a simple value changed comparison is required.
# - can also use nil to detect any change; e.g. in
FromHashList : func(_keylist, _delta, _changed_method)
{
var obj = {parents : [UpdateManager] };
obj.hashkeylist = _keylist;
obj.delta = _delta;
obj.lastval = {};
obj.hashkey = nil;
obj.changed = _changed_method;
obj.needs_update = 0;
obj.isnum = _delta != nil;
obj.update = func(obj)
{
if (me.lastval == nil)
me.needs_update = 1;
else
me.needs_update = 0;
if (obj != nil or me.lastval == nil) {
foreach (hashkey; me.hashkeylist) {
if (me.isnum) {
if (me.lastval[hashkey] == nil or math.abs(me.lastval[hashkey] - obj[hashkey]) >= me.delta) {
me.needs_update = 1;
break;
}
} elsif (me.lastval[hashkey] == nil or me.lastval[hashkey] != obj[hashkey]) {
me.needs_update = 1;
break;
}
}
}
if (me.needs_update) {
me.changed(obj);
foreach (hashkey; me.hashkeylist) {
me.lastval[hashkey] = obj[hashkey];
}
}
};
return obj;
},
};