1
0
Fork 0
fgdata/Nasal/modules/canvas_efis/eicas-message-sys.nas

533 lines
17 KiB
Text

#
# 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;
}, 0, 0);
setlistener(obj.lengthN.getPath(), func(n) {
obj.page_length = n.getValue();
obj.pg_changed = 1;
}, 0, 0);
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") {
logprint(DEV_ALERT, "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") {
logprint(DEV_ALERT, "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;
},
};