1
0
Fork 0

Canvas EFIS framework

This commit is contained in:
Henning Stahlke 2020-02-18 21:18:09 +01:00
parent c6e5244b13
commit b69f460ee5
6 changed files with 1332 additions and 0 deletions

View file

@ -0,0 +1,157 @@
#------------------------------------------
# display-unit.nas - Canvas EFIS framework
# author: jsb
# created: 12/2017
#------------------------------------------
# Class DisplayUnit (DU) - handles a named display 3D object in the cockpit
# * creates a canvas that is placed on the 3D object once
# * creates an image element on canvas to show source input
# * handles power on/off by (un-)hiding canvas root group
var DisplayUnit =
{
#-- static members
_instances: [],
bgcolor: [0.01, 0.01, 0.01, 1],
# call del() on all instances
unload: func() {
foreach (var instance; DisplayUnit._instances) {
instance.del();
}
DisplayUnit._instances = [];
},
del: func() {
if (me.window != nil) {
me.window.del();
me.window = nil;
}
if (me.placement != nil) {
me.placement.remove();
me.placement = nil;
}
if (me.du_canvas != nil) {
me.du_canvas.del();
me.du_canvas = nil;
}
},
# name: string, used in canvas window title and on DU test canvas
# canvas_settings: hash
# screen_obj: string, name of 3D object for canvas placement
# parent_obj: string, optional parent 3D object for placement
new: func(name, canvas_settings, screen_obj, parent_obj = nil) {
var obj = {
parents: [me],
_id: size(DisplayUnit._instances),
canvas_settings: canvas_settings,
placement_node: screen_obj,
placement_parent: parent_obj,
placement: nil,
du_canvas: nil,
root: nil,
window: nil,
name: name,
img: nil, # canvas image element, shall use other canvas as source
powerN: nil,
};
append(DisplayUnit._instances, obj);
return obj._init();
},
_init: func() {
me.canvas_settings["name"] = "DisplayUnit " ~ size(DisplayUnit._instances);
me.du_canvas = canvas.new(me.canvas_settings).setColorBackground(DisplayUnit.bgcolor);
me.root = me.du_canvas.createGroup();
#-- optional for development: create test image
me._test_img();
me.img = me.root.createChild("image", "DisplayUnit "~me.name);
var place = { parent: me.placement_parent, node: me.placement_node };
me.placement = me.du_canvas.addPlacement(place);
#var status = me.placement.getNode("status-msg",1);
return me;
},
_test_img: func() {
var x = num(me.canvas_settings.view[0])/2 or 20;
var y = num(me.canvas_settings.view[1])/2 or 20;
me.root.createChild("text").setText("'"~me.name~"'\n no source ")
.setColor(1,1,1,1)
.setAlignment("center-center")
.setTranslation(x, y);
var L = int(y/16);
var black = [0, 0, 0, 1];
var blue = [0, 0.2, 0.9, 1];
var yellow = [1, 1, 0, 1];
var grey = [0.7, 0.7, 0.7, 1];
var tl = me.root.createChild("group", "top-left");
var r = tl.createChild("path").rect(L, L, 8*L, 8*L)
.setColorFill(grey);
for (var i=1; i <= 3; i += 1) {
for (var j=1; j <= 8; j += 1) {
var r = tl.createChild("path").rect(i*L, j*L, L, L);
# .setStrokeLineWidth(0);
math.mod(i+j, 2) ? r.setColorFill(black) : r.setColorFill(yellow);
}
}
var F = tl.createChild("path")
.setColorFill(blue)
.setColor(blue)
.setStrokeLineJoin("round")
.setStrokeLineWidth(3);
F.moveTo(5*L, 2*L)
.line(3.5*L, 0).line(0, L).line(-2.5*L, 0)
.line(0, L)
.line(2*L, 0).line(0, L).line(-2*L, 0)
.line(0, 3*L)
.line(-L, 0)
.close();
x = me.canvas_settings.view[0]-L;
y = me.canvas_settings.view[1]-L;
me.root.createChild("path", "square-top-left").rect(0, 0, L, L)
.setColorFill(1,1,1,1);
me.root.createChild("path", "square-top-right").rect(x, 0, L, L)
.setColorFill(1,1,1,1);
me.root.createChild("path", "square-btm-left").rect(0, y, L, L)
.setColorFill(1,1,1,1);
me.root.createChild("path", "square-btm-right").rect(x, y, L, L)
.setColorFill(1,1,1,1);
},
# set a new source path for canvas image element
setSource: func(path) {
#print("DisplayUnit.setSource for "~me.du_canvas.getPath()~" ("~me.name~") to "~path);
if (path == "")
me.img.hide();
else {
me.img.set("src", path);
me.img.show();
}
return me;
},
setPowerSource: func(prop, min) {
me.powerN = props.getNode(prop,1);
setlistener(me.powerN, func(n) {
if ((n.getValue() or 0) >= min) me.root.show();
else me.root.hide();
}, 1,0);
},
asWindow: func(window_size) {
me.window = canvas.Window.new(window_size, "dialog");
me.window.set('title', "EFIS " ~ me.name)
.set("resize", 1)
.setCanvas(me.du_canvas);
me.window.move(me._id*200, me._id*40);
if (typeof(me.window.lockAspectRatio) == "func")
me.window.lockAspectRatio(1);
me.window.del = func() { call(canvas.Window.del, [], me); }
return me.window
},
};

View file

@ -0,0 +1,334 @@
#------------------------------------------
# efis-canvas.nas - Canvas EFIS framework
# author: jsb
# created: 12/2017
#------------------------------------------
#-- EFISCanvas - base class to create canvas displays / pages --
# * manages a canvas
# * can load a SVG file and create clipping from <name>_clip elements
# * allows to register multiple update functions with individual update intervals
# * update functions can be en-/disabled by a single property that should
# reflect the visibility of the canvas
# * several listener factories for common animations
var EFISCanvas = {
# static members
_instances: [],
unload: func() {
print("-- Removing EFISCanvas instances --");
foreach (var instance; EFISCanvas._instances) {
print(" - "~instance.name);
instance.del();
}
EFISCanvas._instances = [];
},
# destructor
del: func() {
me._canvas.del();
foreach (var timer; me._timers) {
timer.stop();
}
me._timers = [];
},
colors: EFIS.colors,
defaultcanvas_settings: EFIS.defaultcanvas_settings,
new: func(name, svgfile=nil) {
var obj = {
parents: [me],
_id: size(EFISCanvas._instances), # internal ID for EFIS window mgmt.
id: 0, # instance id e.g. for PFD/MFD
name: name,
# for reload support while efis development
_timers: [],
_canvas: nil,
_root: nil,
svg_keys: [],
updateN: nil, # to be used in update() to pause updates
_instr_props: {},
};
append(EFISCanvas._instances, obj);
obj.updateCountN = EFIS_root_node.getNode("update/count-"~name, 1);
obj.updateCountN.setIntValue(0);
var settings = obj.defaultcanvas_settings;
settings["name"] = name;
obj._canvas = canvas.new(settings);
obj._root = obj._canvas.createGroup();
if (svgfile != nil) {
obj.loadsvg(svgfile);
}
return obj;
},
#used by EFIS._setDisplaySource()
getPath: func {
return me._canvas.getPath();
},
getCanvas: func {
return me._canvas;
},
getRoot: func {
return me._root;
},
#set node that en-/dis-ables canvas updates
setUpdateN: func(n) {
me.updateN = n;
},
loadsvg: func(file) {
var font_mapper = func(family, weight) {
return "LiberationFonts/LiberationSans-Regular.ttf";
};
print("EFIS loading "~file);
canvas.parsesvg(me._root, file, {'font-mapper': font_mapper});
# create nasal variables for SVG elements;
# in a class derived from EFISCanvas, add IDs to the svg_keys member in the constructor (new)
var svg_keys = me.svg_keys;
foreach (var key; svg_keys) {
me[key] = me._root.getElementById(key);
if (me[key] != nil) {
me._updateClip(key);
}
#else print(" loadsvg: invalid key ",key);
}
return me;
},
# register an update function with a certain update interval
# f: function
# f_me: if there is any "me" reference in "f", you can set "me" with this
# defaults to EFISCanvas instance calling this method, useful if "f"
# is a member of the EFISCanvas instance
addUpdateFunction: func(f, interval, f_me = nil) {
if (typeof(f) != "func") {
print("EFISCanvas.addUpdateFunction: Error, argument is not a function.");
return;
}
interval = num(interval);
f_me = (f_me == nil) ? me : f_me;
if (interval != nil and interval >= 0) {
#the updateCountN is meant for debug/performance monitoring
var timer = maketimer(interval, me, func {
if (me.updateN != nil and me.updateN.getValue()) {
var err = [];
call(f, [], f_me, nil, err);
if (size(err))
debug.printerror(err);
me.updateCountN.setValue(me.updateCountN.getValue() + 1);
}
});
append(me._timers, timer);
return timer;
}
},
# start all registered update functions
startUpdates: func() {
foreach (var t; me._timers)
t.start();
},
# stop all registered update functions
stopUpdates: func() {
foreach (var t; me._timers)
t.stop();
},
# getInstr - get props from /instrumentation/<sys>[i]/<prop>
# creates prop node objects for efficient access
# sys: the instrument name (path) (me.id is appended as index!!)
# prop: the property(path)
getInstr: func(sys, prop, default=0, id=nil) {
if (me._instr_props[sys] == nil)
me._instr_props[sys] = {};
if (me._instr_props[sys][prop] == nil) {
if (id == nil) { id = me.id; }
me._instr_props[sys][prop] =
props.getNode("/instrumentation/"~sys~"["~id~"]/"~prop, 1);
}
var value = me._instr_props[sys][prop].getValue();
if (value != nil) return value;
else return default;
},
updateTextElement: func(svgkey, text, color = nil) {
if (me[svgkey] == nil or me[svgkey].setText == nil){
print("updateTextElement(): Invalid argument ", svgkey);
return;
}
me[svgkey].setText(text);
if (color != nil and me[svgkey].setColor != nil) {
if (typeof(color) == "vector") me[svgkey].setColor(color);
else me[svgkey].setColor(me.colors[color]);
}
},
## private methods, to be used in this and derived classes only
_updateClip: func(key) {
var clip_elem = me._root.getElementById(key ~ "_clip");
if (clip_elem != nil) {
clip_elem.setVisible(0);
me[key].setClipByElement(clip_elem);
}
},
# returns generic listener to show/hide element(s)
# svgkeys: can be a string referring to a single element
# or vector of strings referring to SVG elements
# (hint: putting elements in a SVG group (if possible) might be easier)
# value: optional value to trigger show(); otherwise node.value will be implicitly treated as bool
_makeListener_showHide: func(svgkeys, value=nil) {
if (value == nil) {
if (typeof(svgkeys) == "vector")
return func(n) {
if (n.getValue())
foreach (var key; svgkeys) me[key].show();
else
foreach (var key; svgkeys) me[key].hide();
}
else
return func(n) {
if (n.getValue()) me[svgkeys].show();
else me[svgkeys].hide();
}
}
else {
if (typeof(svgkeys) == "vector")
return func(n) {
if (n.getValue() == value)
foreach (var key; svgkeys) me[key].show();
else
foreach (var key; svgkeys) me[key].hide();
};
else
return func(n) {
if (n.getValue() == value) me[svgkeys].show();
else me[svgkeys].hide();
};
}
},
# returns listener to set rotation of element(s)
# svgkeys: can be a string referring to a single element
# or vector of strings referring to SVG elements
# factors: optional, number (if svgkeys is a single key) or hash of numbers
# {"svgkey" : factor}, missing keys will be treated as 1
_makeListener_rotate: func(svgkeys, factors=nil) {
if (factors == nil) {
if (typeof(svgkeys) == "vector")
return func(n) {
var value = n.getValue() or 0;
foreach (var key; svgkeys) {
me[key].setRotation(value);
}
}
else
return func(n) {
var value = n.getValue() or 0;
me[svgkeys].setRotation(value);
}
}
else {
if (typeof(svgkeys) == "vector")
return func(n) {
var value = n.getValue() or 0;
foreach (var key; svgkeys) {
var factor = factors[key] or 1;
me[key].setRotation(value * factor);
}
};
else
return func(n) {
var value = n.getValue() or 0;
var factor = num(factors) or 1;
me[svgkeys].setRotation(value * factor);
};
}
},
# returns listener to set translation of element(s)
# svgkeys: can be a string referring to a single element
# or vector of strings referring to SVG elements
# factors: number (if svgkeys is a single key) or hash of numbers
# {"svgkey" : factor}, missing keys will be treated as 0 (=no op)
_makeListener_translate: func(svgkeys, fx, fy) {
if (typeof(svgkeys) == "vector") {
var x = num(fx) or 0;
var y = num(fy) or 0;
if (typeof(fx) == "hash" or typeof(fy) == "hash") {
return func(n) {
foreach (var key; svgkeys) {
var value = n.getValue() or 0;
if (typeof(fx) == "hash") x = fx[key] or 0;
if (typeof(fy) == "hash") y = fy[key] or 0;
me[key].setTranslation(value * x, value * y);
}
};
}
else {
return func(n) {
foreach (var key; svgkeys) {
var value = n.getValue() or 0;
me[key].setTranslation(value * x, value * y);
}
};
}
}
else {
if (num(fx) == nil or num(fy) == nil) {
print("EFISCanvas._makeListener_translate(): Error, factor not a number.");
return func ;
}
return func(n) {
var value = n.getValue() or 0;
if (num(value) == nil)
value = 0;
me[svgkeys].setTranslation(value * fx, value * fy);
};
}
},
# returns generic listener to change element color
# svgkeys: can be a string referring to a single element
# or vector of strings referring to SVG elements
# (hint: putting elements in a SVG group (if possible) might be easier)
# colors can be either a vector e.g. [r,g,b] or "name" from me.colors
_makeListener_setColor: func(svgkeys, color_true, color_false) {
var col_0 = (typeof(color_false) == "vector") ? color_false : me.colors[color_false];
var col_1 = (typeof(color_true) == "vector") ? color_true : me.colors[color_true];
if (typeof(svgkeys) == "vector") {
return func(n) {
if (n.getValue())
foreach (var key; svgkeys) me[key].setColor(col_1);
else
foreach (var key; svgkeys) me[key].setColor(col_0);
};
}
else {
return func(n) {
if (n.getValue()) me[svgkeys].setColor(col_1);
else me[svgkeys].setColor(col_0);
};
}
},
_makeListener_updateText: func(svgkeys, format="%s", default="") {
if (typeof(svgkeys) == "vector") {
return func(n) {
foreach (var key; svgkeys)
me.updateTextElement(key, sprintf(format, n.getValue() or default));
};
}
else {
return func(n) {
me.updateTextElement(svgkeys, sprintf(format, n.getValue() or default));
};
}
},
};

View file

@ -0,0 +1,9 @@
#------------------------------------------
# efis-framework.nas - Canvas EFIS framework
# author: jsb
#------------------------------------------
var EFIS_root_node = props.getNode("/efis", 1);
io.include("display-unit.nas");
io.include("efis.nas");
io.include("efis-canvas.nas");

View file

@ -0,0 +1,284 @@
#-------------------------------------------------------------------------------
# efis.nas
# author: jsb
# created: 12/2017
#-------------------------------------------------------------------------------
# class EFIS
# manage cockpit displays (=outputs) and sources (image generators for PFD, MFD, EICAS...)
# allow redirection of sources to alternate displays (allow for simulated display fault)
var EFIS = {
#-- static members
_instances: [],
unload: func() {
foreach (var instance; EFIS._instances) {
instance.del();
}
EFIS._instances = [];
},
NO_SRC: -1,
defaultcanvas_settings: {
"name": "EFIS_display",
"size": [1024,1024],
"view": [1024,1024],
"mipmapping": 1
},
window_size: [450,450],
colors: {
transparent: [1,0,0,0],
white: [1,1,1],
red: [1,0,0],
green : [0,1,0],
blue : [0,0,1],
yellow: [1,1,0],
cyan: [0,1,1],
magenta: [1,0,1],
amber: [1,0.682,0],
},
del: func() {
},
# create EFIS object
# display_names: vector of display names, one DisplayUnit per entry will be
# created
# object_names: vector of same size and order as display_names, containing
# 3D object names for canvas placement of the DisplayUnits
new: func(display_names, object_names, canvas_settings=nil) {
if (typeof(display_names) != "vector") {
printlog("error", "EFIS.new: 'display_names' not a vector!");
return;
}
var obj = {
parents: [me],
id: 0,
display_units: [],
sources: [], # vector of EFISCanvas instances
display_names: display_names,
controls: {},
source_records: [], # stores infos about each source
active_sources: [],
powerN: nil,
};
if (object_names != nil and typeof(object_names) == "vector"
and size(display_names) == size(object_names))
{
foreach (var i; display_names) {
append(obj.active_sources, EFIS.NO_SRC);
}
var settings = obj.defaultcanvas_settings;
if (canvas_settings != nil and typeof(canvas_settings) == "hash") {
foreach (var key; keys(canvas_settings)) {
settings[key] = canvas_settings[key];
}
}
setsize(obj.display_units, size(display_names));
forindex (var id; display_names)
{
obj.display_units[id] = DisplayUnit.new(obj.display_names[id],
obj.defaultcanvas_settings, object_names[id]);
}
}
append(EFIS._instances, obj);
return obj;
}, #new
#-- private methods ----------------------
# _setDisplaySource - switch display unit du_id to source source_id
# count how often a source is displayed, sources not displayed stop updating themselves
_setDisplaySource: func(du_id, source_id)
{
var prev_source = me.active_sources[du_id];
#print("setDisplaySource unit "~du_id~" src "~source_id~" prev "~prev_source);
if (prev_source >= 0) {
if (me.source_records[prev_source] == nil)
printlog("error", "_setDisplaySource error: prev: "~prev_source~" #"~size(me.source_records));
var n = me.source_records[prev_source].visibleN;
n.setValue(n.getValue() - 1);
}
var path = "";
if (source_id >= 0) {
path = me.sources[source_id].getPath();
}
me.display_units[du_id].setSource(path);
me.active_sources[du_id] = source_id;
var n = me.source_records[source_id].visibleN;
n.setValue(n.getValue() + 1);
},
# mapping can be either:
# - vector of source ids, size must equal size(display_units)
# values nil = do nothing, 0..N select source, -1 no source
# - hash {<unit_name>: source_id}
_activateRouting: func(mapping)
{
if (typeof(mapping) == "vector") {
forindex (var unit_id; me.display_units)
{
if (mapping[unit_id] != nil)
me._setDisplaySource(unit_id, mapping[unit_id]);
}
}
elsif (typeof(mapping) == "hash") {
foreach (var unit_name; keys(mapping))
{
forindex (var unit_id; me.display_names) {
if (me.display_names[unit_id] == unit_name) {
me._setDisplaySource(unit_id, mapping[unit_name]);
}
}
}
}
},
# Start/stop updates on all sources
_powerOnOff: func(power) {
if (power) {
logprint(3, "EFIS power on");
foreach (var src; me.sources)
src.startUpdates();
}
else {
logprint(3, "EFIS power off.");
foreach (var src; me.sources)
src.stopUpdates();
}
},
#-- public methods -----------------------
# set power prop and add listener to start/stop all registered update functions
# e.g. power up will start updates, loss of power will stop updates
setPowerProp: func(path) {
me.powerN = props.getNode(path,1);
setlistener(me.powerN, func(n) {
var power = n.getValue();
me._powerOnOff(power);
}, 1, 0);
},
setWindowSize: func(window_size) {
if (window_size != nil and typeof(window_size) == "vector") {
me.window_size = window_size;
}
else {
logprint(5, "EFIS.setWindowSize(): Error, argument is not a vector.");
}
},
boot: func() {
me._powerOnOff(me.powerN.getValue());
},
setDUPowerProps: func(power_props, minimum_power=0) {
if (power_props != nil and typeof(power_props) == "vector") {
forindex (var i; me.display_names) {
me.display_units[i].setPowerSource(power_props[i], minimum_power);
}
}
else logprint(5, "EFIS.setDUPowerProps(): Error, argument is not a vector.");
},
# add a EFISCanvas instance as display source
# EFIS controls updating by tracking how often source is used
# returns source ID that can be used in mappings
addSource: func(efis_canvas) {
append(me.sources, efis_canvas);
var srcID = size(me.sources) - 1;
var visibleN = EFIS_root_node.getNode("update/visible"~srcID,1);
visibleN.setIntValue(0);
efis_canvas.setUpdateN(visibleN);
append(me.source_records, {visibleN: visibleN});
return srcID;
},
# ctrl: property path to integer prop
# mappings: vector of display mappings
# callback: optional function that will be called with current ctrl value
addDisplaySwapControl: func(ctrl, mappings, callback=nil)
{
if (me.controls[ctrl] != nil) return;
ctrlN = props.getNode(ctrl,1);
if (typeof(mappings) != "vector") {
logprint(5, "EFIS addDisplayControl: mappings must be a vector.");
return;
}
var listener = func(p) {
var ctlValue = p.getValue();
if (ctlValue >= 0 and ctlValue < size(me.controls[ctrl].mappings))
me._activateRouting(me.controls[ctrl].mappings[ctlValue]);
else debug.warn("Invalid value for display selector "~ctrl~": "~ctlValue);
if (callback != nil) {
call(callback, [ctlValue], nil, nil, var err = []);
debug.printerror(err);
}
}
#print("addDisplayControl "~ctrl);
me.controls[ctrl] = {L: setlistener(ctrlN, listener, 0, 0), mappings: mappings};
},
# selected: property (node or path) containing source number (integer)
# target: contains the DU number to which the source will be mapped
# sources: optional vector, selected -> source ID (as returned by addSource)
# defaults to all registered sources
addSourceSelector: func(selected, target, sources=nil){
if (typeof(selected) == "scalar") {
selected = props.getNode(selected,1);
}
if (typeof(target) == "scalar") {
target = props.getNode(target,1);
}
if (selected.getValue() == nil)
selected.setIntValue(0);
if (sources == nil) {
for (var i = 0; i < size(me.sources); i += 1)
append(sources, i);
}
setlistener(selected, func(n){
var src = n.getValue();
var destination = target.getValue();
if (src >= 0 and src < size(sources))
me._setDisplaySource(destination, sources[src]);
});
},
setDefaultMapping: func(mapping) {
if (mapping != nil and (typeof(mapping) == "vector" or typeof(mapping) == "hash")) {
me.default_mapping = mapping;
me._activateRouting(me.default_mapping);
}
},
getDU: func(i) {return me.display_units[i]},
#getSources: func() { return me.source_records; },
getDisplayName: func(id) {
id = num(id);
if (id != nil and id >=0 and id < size(me.display_names))
return me.display_names[id];
else return "Invalid display ID.";
},
getDisplayID: func(name) {
for (var id = 0; id < size(me.display_names); id += 1) {
if (me.display_names[id] == name) return id;
}
return -1;
},
#open a canvas window for display unit <id>
displayWindow: func(id)
{
id = num(id);
if (id < 0 or id >= size(me.display_units))
{
debug.warn("EFIS.displayWindow: invalid id");
return;
}
return me.display_units[id].asWindow(me.window_size);
},
};

View file

@ -0,0 +1,533 @@
#
# EICAS message system
# initial version by jsb 05/2018
#
# simple pager to get a sub vector of messages
var Pager = {
new: func(page_length, prop_path) {
var obj = {
parents: [me],
page_length: 1,
lengthN: props.getNode(prop_path~"/page_length",1),
current_page: 1,
pageN: props.getNode(prop_path~"/page",1),
pagesN: props.getNode(prop_path~"/pages",1),
last_result: 0,
prop_path: prop_path,
line_count: 0,
pg_changed: 0,
};
obj.setPageLength(page_length);
obj.setPage(1);
setlistener(obj.pageN.getPath(), func(n) {
obj.current_page = n.getValue();
obj.pg_changed = 1;
});
setlistener(obj.lengthN.getPath(), func(n) {
obj.page_length = n.getValue();
obj.pg_changed = 1;
});
return obj;
},
pageChanged: func() {
var c = me.pg_changed;
me.pg_changed = 0;
return c;
},
setPageLength: func(n) {
me.page_length = int(n) or 1;
me.lengthN.setIntValue(me.page_length);
return me;
},
setPage: func(p) {
me.current_page = int(p) or 1;
me.pageN.setIntValue(me.current_page);
return me;
},
getPageCount: func() {
return me.page_count;
},
getCurrentPage: func() {
return me.current_page;
},
# lines: vector of all messages
# returns lines of current page; sticky lines will not be paged
page: func(lines) {
me.line_count = size(lines);
#count sticky lines, assume sorted list
for (var sticky = 0; sticky < me.line_count; sticky += 1) {
if (lines[sticky].paging) break;
}
if (sticky >= me.page_length) {
sticky = me.page_length;
me.page_count = 1;
}
else {
var page_lines = me.line_count - sticky;
var len = me.page_length - sticky;
me.page_count = int((page_lines - 1) / len) + 1;
}
me.pagesN.setValue(me.page_count);
me.current_page = me.pageN.getValue() or 1;
var start = len * (me.current_page-1) + sticky;
# default to first page if page is invalid
if (me.current_page > me.page_count) {
me.setPage(1);
start = sticky;
}
var end = start + len - 1;
if (end >= me.line_count)
end = me.line_count-1;
#print("page l:"~me.line_count~" start "~start~" end "~end);
var result = sticky ? lines[0:(sticky-1)] : [];
if (start <= end) {
return result~lines[start:end];
}
else return result;
},
};
var MessageClass = {
#static, increased by new()
prio: 0,
new: func(name, paging, prio=0) {
var obj = {
parents: [me],
name: name,
paging: paging,
disabled: 0,
color: [1,1,1],
};
if (prio)
obj.prio = prio;
else {
obj.prio = me.prio;
me.prio += 1;
}
return obj;
},
setColor: func(color) {
if (color != nil)
me.color = color;
return me;
},
setPrio: func(prio) {
me.prio = int(prio);
return me;
},
enable: func { me.disabled = 0; },
disable: func(bool = 1) { me.disabled = bool; },
isDisabled: func { return me.disabled; },
};
var Message = {
msg: "",
prop: "",
aural: "",
condition: {
eq: "equals",
ne: "not equals",
lt: "less than",
gt: "greater than",
},
};
var MessageSystem = {
PAGING: 1,
NO_PAGING: 0,
new: func(page_length, prop_path) {
var obj = {
parents: [me],
rootN : props.getNode(prop_path,1),
page_length: page_length,
pager: Pager.new(page_length, prop_path),
classes: [],
messages: [], # vector of vector of messages
sounds: {},
sound_path: "",
sound_queue: "efis",
active_messages: [], # lists of active message IDs per class
active_aurals: {}, # list of active aural warnings ((un-)set if corresponding message is (in-)active)
msg_list: [], # active message list (flat, sorted by class)
first_changed_line: 0, # for later optimisation: first changed line in msg_list
changed: 1,
powerN: nil,
canvas_group: nil,
page_indicator: nil,
page_indicator_format: "Page %2d/%2d",
lines: [],
};
return obj;
},
# setRootNode: func(n) {
# me.rootN = n;
# },
# set power prop and add listener to start/stop all registered update functions
setPowerProp: func(p) {
me.powerN = props.getNode(p,1);
setlistener(me.powerN, func(n) {
if (n.getValue()) {
me.init();
}
}, 1, 0);
},
# class_name: identifier for msg class
# paging: true = normal paging, false = msg class is sticky at top of list
# returns class id (int)
addMessageClass: func(class_name, paging, color = nil) {
var class = size(me.classes);
me["new-msg"~class] = me.rootN.getNode("new-msg-"~class_name,1);
me["new-msg"~class].setIntValue(0);
append(me.classes, MessageClass.new(class_name, paging).setColor(color));
append(me.active_messages, []);
return class;
},
# addMessages creates a new msg class and add messages to it
# class: class id returned by addMessageClass();
# messages: vector of message objects (hashes)
addMessages: func(class, messages) {
forindex (var i; messages) {
messages[i]["_class"] = class;
}
append(me.messages, messages);
var simpleL = func(i){
return func(n) {
var val = n.getValue() or 0;
me.setMessage(class, i, val);
}
};
var eqL = func(i) {
return func(n) {
var val = n.getValue() or 0;
if (val == messages[i].condition["eq"])
me.setMessage(class, i, 1);
else me.setMessage(class, i, 0);
}
};
var neL = func(i) {
return func(n) {
var val = n.getValue() or 0;
if (val != messages[i].condition["ne"])
me.setMessage(class, i, 1);
else me.setMessage(class, i, 0);
}
};
var ltL = func(i) {
return func(n) {
var val = num(n.getValue()) or 0;
if (val < messages[i].condition["lt"])
me.setMessage(class, i, 1);
else me.setMessage(class, i, 0);
}
};
var gtL = func(i) {
return func(n) {
var val = num(n.getValue()) or 0;
if (val > messages[i].condition["gt"])
me.setMessage(class, i, 1);
else me.setMessage(class, i, 0);
}
};
forindex (var i; messages) {
if (messages[i].prop) {
#print("addMessage "~i~" t:"~messages[i].msg~" p:"~messages[i].prop);
var prop = props.getNode(messages[i].prop,1);
# listeners won't work on aliases so find real node
while (prop.getAttribute("alias")) {
prop = prop.getAliasTarget();
}
if (messages[i]["condition"] != nil) {
var c = messages[i]["condition"];
if (c["eq"] != nil) setlistener(prop, eqL(i), 1, 0);
if (c["ne"] != nil) setlistener(prop, eqL(i), 1, 0);
if (c["lt"] != nil) setlistener(prop, ltL(i), 1, 0);
if (c["gt"] != nil) setlistener(prop, gtL(i), 1, 0);
}
else setlistener(prop, simpleL(i), 1, 0);
}
}
},
setSoundPath: func(path) {
me.sound_path = path;
},
addAuralAlert: func(id, filename, volume=1.0, path=nil) {
if (typeof(id) == "hash") {
printlog(5, "First argument to addAuralAlert() must be a string but is a hash. Use addAuralAlerts() to pass all alerts in one hash.");
}
me.sounds[id] = {
path: (path == nil) ? me.sound_path : path,
file: filename,
volume: volume,
queue: me.sound_queue,
};
me.active_aurals[id] = 0;
},
addAuralAlerts: func(alert_hash) {
if (typeof(alert_hash) != "hash") {
print("MessageSystem.addAuralAlerts: parameter must be a hash!");
return;
}
me.sounds = alert_hash;
foreach (var k; keys(alert_hash)){
me.active_aurals[k] = 0;
if (typeof(me.sounds[k]) == "scalar") {
me.sounds[k] = {file: me.sounds[k]};
}
me.sounds[k]["queue"] = me.sound_queue;
if (me.sounds[k]["path"] == nil) {
me.sounds[k]["path"] = me.sound_path;
}
if (me.sounds[k]["volume"] == nil) {
me.sounds[k]["volume"] = 1.0;
}
}
},
# check per class queues and create list of all active messages not inhibited
_updateList: func() {
me.msg_list = [];
forindex (var class; me.active_messages) {
foreach (var id; me.active_messages[class]) {
if (!me.classes[class].disabled and !me.messages[class][id]["disabled"])
append(me.msg_list, {
text: me.messages[class][id].msg,
color: me.classes[class].color,
paging: me.classes[class].paging });
}
}
},
_remove: func(class, msg) {
var tmp = [];
for (var i = 0; i < size(me.active_messages[class]); i += 1) {
if (me.active_messages[class][i] != msg) {
append(tmp, me.active_messages[class][i]);
}
}
return tmp;
},
_isActive: func(class, msg) {
foreach (var m; me.active_messages[class]) {
if (m == msg) {
return 1;
}
}
return 0;
},
# (de-)activate message
setMessage: func(class, msg_id, visible=1) {
if (class >= size(me.classes))
return;
var isActive = me._isActive(class, msg_id);
if ((isActive and visible) or (!isActive and !visible)) {
# no change
return;
}
if (!me.changed) {
me.first_changed_line = me.pager.page_length;
}
#add message at head of list, 2DO: priority handling?!
var aural = me.messages[class][msg_id]["aural"];
if (visible) {
me.active_messages[class] = [msg_id]~me.active_messages[class];
# set new-msg flag in prop tree, e.g. to trigger sounds;
# may be reset from outside this class so we can trigger again here
me["new-msg"~class].setIntValue(1);
if (aural != nil) {
me.active_aurals[aural] = 1;
me.auralAlert(aural);
}
}
else {
me.active_messages[class] = me._remove(class, msg_id);
if (aural != nil) me.active_aurals[aural] = 0;
# clear new-msg flag if last message is gone
if (size(me.active_messages[class]) == 0)
me["new-msg"~class].setIntValue(-1);
}
# count lines of classes with higher priority (= lower class id)
# we do not need to update them as they did not change
var unchanged = 0;
for (var c = 0; c < class; c += 1) {
unchanged += size(me.active_messages[c]);
}
if (me.first_changed_line > unchanged) {
me.first_changed_line = unchanged;
}
#print("set c:"~class~" m:"~msg_id~" v:"~visible~ " 1upd:"~me.first_changed_line);
# me._updateList();
me.changed = 1;
},
auralAlert: func(aural) {
if (me.sounds != nil and aural != nil) {
if (me.active_aurals[aural])
fgcommand("play-audio-sample", props.Node.new(me.sounds[aural]));
}
},
#-- check for active messages and set new-msg flags.
# can be used on power up to trigger new-msg events.
init: func {
forindex (var class; me.active_messages) {
if (size(me.active_messages[class])) {
me["new-msg"~class].setIntValue(1);
}
}
me.changed = 1;
#hack for aural alerts
#print("Enabling EICAS Message System sounds: /sim/sound/"~me.sound_queue~"/enabled = 1");
setprop("/sim/sound/"~me.sound_queue~"/enabled", 1);
},
hasUpdate: func {
return me.changed;
},
setPageLength: func(p) {
if (p > size(me.lines))
return;
for (var i = math.min(p, me.page_length); i < size(me.lines); i += 1) {
me.lines[i].setText("");
}
me.page_length = p;
me.pager.setPageLength(p);
me.updateCanvas();
return me;
},
getPageLength: func {
return me.page_length;
},
getFirstUpdateIndex: func {
return me.first_changed_line;
},
# returns message queue and clears the hasUpdate flag
getActiveMessages: func {
if (me.changed) {
me._updateList();
}
me.changed = 0;
return me.msg_list;
},
#find message text, return id
getMessageID: func(class, msgtext) {
forindex (var id; me.messages[class]) {
if (me.messages[class][id].msg == msgtext)
return id;
}
return -1;
},
# inhibit message id (or all messages in class if no id is given)
disableMessage: func(class, id = nil) {
if (id != nil)
me.messages[class][id]["disabled"] = 1;
else forindex (var i; me.messages[class])
me.messages[class][i]["disabled"] = 1;
},
# re-enable message id (or all messages in class if no id is given)
enableMessage: func(class, id = nil) {
if (id != nil)
me.messages[class][id]["disabled"] = 0;
else forindex (var i; me.messages[class])
me.messages[class][i]["disabled"] = 0;
},
#
#-- following methods are for message output on a canvas --
#
# pass an existing canvas group to create text elements on
setCanvasGroup: func(group) {
me.canvas_group = group;
return me;
},
# create text elements for message lines in canvas group; call setCanvasGroup() first!
createCanvasTextLines: func(left, top, line_spacing, font_size) {
me.lines = me.canvas_group.createChildren("text", me.page_length);
forindex (var i; me.lines) {
var l = me.lines[i];
l.setAlignment("left-top").setTranslation(left, top + i*line_spacing);
l.setFont("LiberationFonts/LiberationSans-Regular.ttf");
l.setFontSize(font_size);
}
return me.lines;
},
# create text element for "page i of N"; call setCanvasGroup() first!
# returns the text element
createPageIndicator: func(left, top, font_size, format_string = nil) {
me.page_indicator = me.canvas_group.createChild("text");
me.page_indicator.setAlignment("left-top").setTranslation(left, top);
me.page_indicator.setFont("LiberationFonts/LiberationSans-Regular.ttf");
me.page_indicator.setFontSize(font_size);
if (format_string != nil)
me.page_indicator_format = format_string;
return me.page_indicator;
},
# call this regularly to update text lines on canvas
updateCanvas: func() {
# check if page change was selected
var pg_changed = me.pager.pageChanged();
if (!(pg_changed or me.changed))
return 0;
if (pg_changed) {
# update all lines on screen
me.first_changed_line = 0;
}
me._updateList();
me.changed = 0;
var messages = me.pager.page(me.msg_list);
for (var i = me.first_changed_line; i < size(messages); i += 1) {
me.lines[i].setText(messages[i].text);
if (messages[i].color != nil)
me.lines[i].setColor(messages[i].color);
}
# clear text from unused lines
for (i; i < me.page_length; i += 1) {
me.lines[i].setText("");
}
if (me.page_indicator != nil) {
if (me.pager.getPageCount() > 1) {
me.page_indicator.show();
me.updatePageIndicator(me.pager.getCurrentPage(), me.pager.getPageCount());
}
else me.page_indicator.hide();
}
},
updatePageIndicator: func(current, total) {
me.page_indicator.setText(sprintf(me.page_indicator_format, current, total));
return me;
},
};

View file

@ -0,0 +1,15 @@
#
# canvas-efis loader
#
var EFIS_namespace = "canvas_efis";
var unload = func(module) {
globals[EFIS_namespace].DisplayUnit.unload();
globals[EFIS_namespace].EFISCanvas.unload();
globals[EFIS_namespace].EFIS.unload();
}
var main = func(module) {
io.load_nasal(module.getFilePath()~"efis-framework.nas", EFIS_namespace);
io.load_nasal(module.getFilePath()~"eicas-message-sys.nas", EFIS_namespace);
}