diff --git a/Nasal/modules/canvas_efis/display-unit.nas b/Nasal/modules/canvas_efis/display-unit.nas new file mode 100644 index 000000000..f56e84749 --- /dev/null +++ b/Nasal/modules/canvas_efis/display-unit.nas @@ -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 + }, +}; diff --git a/Nasal/modules/canvas_efis/efis-canvas.nas b/Nasal/modules/canvas_efis/efis-canvas.nas new file mode 100644 index 000000000..f17215731 --- /dev/null +++ b/Nasal/modules/canvas_efis/efis-canvas.nas @@ -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 _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/[i]/ + # 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)); + }; + } + }, +}; diff --git a/Nasal/modules/canvas_efis/efis-framework.nas b/Nasal/modules/canvas_efis/efis-framework.nas new file mode 100644 index 000000000..dca3c2afa --- /dev/null +++ b/Nasal/modules/canvas_efis/efis-framework.nas @@ -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"); diff --git a/Nasal/modules/canvas_efis/efis.nas b/Nasal/modules/canvas_efis/efis.nas new file mode 100644 index 000000000..9dcbbf2d2 --- /dev/null +++ b/Nasal/modules/canvas_efis/efis.nas @@ -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 {: 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 + 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); + }, +}; diff --git a/Nasal/modules/canvas_efis/eicas-message-sys.nas b/Nasal/modules/canvas_efis/eicas-message-sys.nas new file mode 100644 index 000000000..5aae2f353 --- /dev/null +++ b/Nasal/modules/canvas_efis/eicas-message-sys.nas @@ -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; + }, +}; diff --git a/Nasal/modules/canvas_efis/main.nas b/Nasal/modules/canvas_efis/main.nas new file mode 100644 index 000000000..614646781 --- /dev/null +++ b/Nasal/modules/canvas_efis/main.nas @@ -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); +}