## # Node class definition. The class methods simply wrap the # low level extension functions which work on a "ghost" handle to a # 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)), 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), toggleBoolValue: func _toggleBoolValue(me._g, arg), setDoubleValue : func _setDoubleValue(me._g, arg), # 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(), 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 (defaultValue = 0) { var v = me.getValue(); if (!v) { return defaultValue; } return math.round(v); }, getDoubleValue : func (defaultValue = 0.0) { var v = me.getValue(); if (!v) { return defaultValue; } return num(v); }, remove : func { if((var p = me.getParent()) == nil) return nil; p.removeChild(me.getName(), me.getIndex()); }, # 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)) 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 { 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 { 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; }, }; ## # 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; } ## # 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 { 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()); } ## # 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 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 property branch according to the rules # 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()); 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 { 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 { 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 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 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; }, };