# HG changeset patch
# User Edouard Tisserant
# Date 1664953578 -7200
# Node ID f117526d41bae50c47bb782d17396c3111a9ede1
# Parent c1796e57affd2530bd11e5777edba13b324892b9
SVGHMI: update generated XSLT
diff -r c1796e57affd -r f117526d41ba svghmi/analyse_widget.xslt
--- a/svghmi/analyse_widget.xslt Tue Oct 04 11:06:04 2022 +0200
+++ b/svghmi/analyse_widget.xslt Wed Oct 05 09:06:18 2022 +0200
@@ -262,6 +262,42 @@
speed
+
+
+
+
+
+
+
+ Arguments are either:
+
+
+
+ - name=value: setting variable with literal value.
+
+ - name=other_name: copy variable content into another
+
+
+
+ "active"+"inactive" labeled elements can be provided to show feedback when pressed
+
+
+
+ Exemples:
+
+
+
+ HMI:Assign:notify=1@notify=/PLCVAR
+
+ HMI:Assign:ack=2:notify=1@ack=.local_var@notify=/PLCVAR
+
+
+
+
+
+ Assign variables on click
+
+
diff -r c1796e57affd -r f117526d41ba svghmi/gen_index_xhtml.xslt
--- a/svghmi/gen_index_xhtml.xslt Tue Oct 04 11:06:04 2022 +0200
+++ b/svghmi/gen_index_xhtml.xslt Wed Oct 05 09:06:18 2022 +0200
@@ -555,6 +555,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -657,7 +674,8 @@
-
+
+
@@ -890,6 +908,14 @@
+ DISCARDABLES:
+
+
+
+
+
+
+
In Foreach:
@@ -945,6 +971,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1516,8 +1557,6 @@
var cache = hmitree_types.map(_ignored => undefined);
- var updates = new Map();
-
function page_local_index(varname, pagename){
@@ -1530,7 +1569,7 @@
new_index = next_available_index++;
- hmi_locals[pagename] = {[varname]:new_index}
+ hmi_locals[pagename] = {[varname]:new_index};
} else {
@@ -1556,8 +1595,6 @@
cache[new_index] = defaultval;
- updates.set(new_index, defaultval);
-
if(persistent_locals.has(varname))
persistent_indexes.set(new_index, varname);
@@ -2656,6 +2693,199 @@
}
+
+
+
+
+
+
+
+ Arguments are either:
+
+
+
+ - name=value: setting variable with literal value.
+
+ - name=other_name: copy variable content into another
+
+
+
+ "active"+"inactive" labeled elements can be provided to show feedback when pressed
+
+
+
+ Exemples:
+
+
+
+ HMI:Assign:notify=1@notify=/PLCVAR
+
+ HMI:Assign:ack=2:notify=1@ack=.local_var@notify=/PLCVAR
+
+
+
+
+
+ Assign variables on click
+
+
+
+ class
+ AssignWidget
+ extends Widget{
+
+ frequency = 2;
+
+
+
+ onmouseup(evt) {
+
+ svg_root.removeEventListener("pointerup", this.bound_onmouseup, true);
+
+ if(this.enable_state) {
+
+ this.activity_state = false
+
+ this.request_animate();
+
+ this.assign();
+
+ }
+
+ }
+
+
+
+ onmousedown(){
+
+ if(this.enable_state) {
+
+ svg_root.addEventListener("pointerup", this.bound_onmouseup, true);
+
+ this.activity_state = true;
+
+ this.request_animate();
+
+ }
+
+ }
+
+
+
+ }
+
+
+
+
+
+
+
+
+ /disabled
+
+
+
+
+
+
+ activable_sub:{
+
+
+
+
+
+ /active /inactive
+
+
+ no
+
+
+
+
+
+ },
+
+ has_activity:
+
+ ,
+
+ init: function() {
+
+ this.bound_onmouseup = this.onmouseup.bind(this);
+
+ this.element.addEventListener("pointerdown", this.onmousedown.bind(this));
+
+ },
+
+ assignments: {},
+
+ dispatch: function(value, oldval, varnum) {
+
+
+
+
+
+
+
+
+ if(varnum ==
+
+ ) this.assignments["
+
+ "] = value;
+
+
+
+
+
+ },
+
+ assign: function() {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ const
+
+ = this.assignments["
+
+ "];
+
+ if(
+
+ != undefined)
+
+ this.apply_hmi_value(
+
+ ,
+
+ );
+
+
+
+ this.apply_hmi_value(
+
+ ,
+
+ );
+
+
+
+
+ },
+
+
@@ -5923,39 +6153,31 @@
frequency = 2;
-
-
- make_on_click() {
-
- let that = this;
-
- const name = this.args[0];
-
- return function(evt){
-
- /* TODO: in order to allow jumps to page selected through
-
- for exemple a dropdown, support path pointing to local
-
- variable whom value would be an HMI_TREE index and then
-
- jump to a relative page not hard-coded in advance
-
- */
-
- if(that.enable_state) {
-
- const index =
-
- (that.is_relative && that.indexes.length > 0) ?
-
- that.indexes[0] + that.offset : undefined;
-
- fading_page_switch(name, index);
-
- that.notify();
-
- }
+ target_page_is_current_page = false;
+
+ button_beeing_pressed = false;
+
+
+
+ onmouseup(evt) {
+
+ svg_root.removeEventListener("pointerup", this.bound_onmouseup, true);
+
+ if(this.enable_state) {
+
+ const index =
+
+ (this.is_relative && this.indexes.length > 0) ?
+
+ this.indexes[0] + this.offset : undefined;
+
+ this.button_beeing_pressed = false;
+
+ this.activity_state = this.target_page_is_current_page || this.button_beeing_pressed;
+
+ fading_page_switch(this.args[0], index);
+
+ this.notify();
}
@@ -5963,6 +6185,24 @@
+ onmousedown(){
+
+ if(this.enable_state) {
+
+ svg_root.addEventListener("pointerup", this.bound_onmouseup, true);
+
+ this.button_beeing_pressed = true;
+
+ this.activity_state = true;
+
+ this.request_animate();
+
+ }
+
+ }
+
+
+
notify_page_change(page_name, index) {
// called from animate()
@@ -5973,7 +6213,9 @@
const ref_name = this.args[0];
- this.activity_state = ((ref_name == undefined || ref_name == page_name) && index == ref_index);
+ this.target_page_is_current_page = ((ref_name == undefined || ref_name == page_name) && index == ref_index);
+
+ this.activity_state = this.target_page_is_current_page || this.button_beeing_pressed;
// Since called from animate, update activity directly
@@ -6031,7 +6273,9 @@
init: function() {
- this.element.onclick = this.make_on_click();
+ this.bound_onmouseup = this.onmouseup.bind(this);
+
+ this.element.addEventListener("pointerdown", this.onmousedown.bind(this));
this.activable = true;
@@ -11091,1000 +11335,1064 @@
- // Apply updates recieved through ws.onmessage to subscribed widgets
-
- function apply_updates() {
-
- updates.forEach((value, index) => {
+ // Called on requestAnimationFrame, modifies DOM
+
+ var requestAnimationFrameID = null;
+
+ function animate() {
+
+ let rearm = true;
+
+ do{
+
+ if(page_fading == "pending" || page_fading == "forced"){
+
+ if(page_fading == "pending")
+
+ svg_root.classList.add("fade-out-page");
+
+ page_fading = "in_progress";
+
+ if(page_fading_args.length)
+
+ setTimeout(function(){
+
+ switch_page(...page_fading_args);
+
+ },1);
+
+ break;
+
+ }
+
+
+
+ // Do the page swith if pending
+
+ if(page_switch_in_progress){
+
+ if(current_subscribed_page != current_visible_page){
+
+ switch_visible_page(current_subscribed_page);
+
+ }
+
+
+
+ page_switch_in_progress = false;
+
+
+
+ if(page_fading == "in_progress"){
+
+ svg_root.classList.remove("fade-out-page");
+
+ page_fading = "off";
+
+ }
+
+ }
+
+
+
+ if(jumps_need_update) update_jumps();
+
+
+
+
+
+ pending_widget_animates.forEach(widget => widget._animate());
+
+ pending_widget_animates = [];
+
+ rearm = false;
+
+ } while(0);
+
+
+
+ requestAnimationFrameID = null;
+
+
+
+ if(rearm) requestHMIAnimation();
+
+ }
+
+
+
+ function requestHMIAnimation() {
+
+ if(requestAnimationFrameID == null){
+
+ requestAnimationFrameID = window.requestAnimationFrame(animate);
+
+ }
+
+ }
+
+
+
+ // Message reception handler
+
+ // Hash is verified and HMI values updates resulting from binary parsing
+
+ // are stored until browser can compute next frame, DOM is left untouched
+
+ ws.onmessage = function (evt) {
+
+
+
+ let data = evt.data;
+
+ let dv = new DataView(data);
+
+ let i = 0;
+
+ try {
+
+ for(let hash_int of hmi_hash) {
+
+ if(hash_int != dv.getUint8(i)){
+
+ throw new Error("Hash doesn't match");
+
+ };
+
+ i++;
+
+ };
+
+
+
+ while(i < data.byteLength){
+
+ let index = dv.getUint32(i, true);
+
+ i += 4;
+
+ let iectype = hmitree_types[index];
+
+ if(iectype != undefined){
+
+ let dvgetter = dvgetters[iectype];
+
+ let [value, bytesize] = dvgetter(dv,i);
+
+ dispatch_value(index, value);
+
+ i += bytesize;
+
+ } else {
+
+ throw new Error("Unknown index "+index);
+
+ }
+
+ };
+
+
+
+ // register for rendering on next frame, since there are updates
+
+ } catch(err) {
+
+ // 1003 is for "Unsupported Data"
+
+ // ws.close(1003, err.message);
+
+
+
+ // TODO : remove debug alert ?
+
+ alert("Error : "+err.message+"\nHMI will be reloaded.");
+
+
+
+ // force reload ignoring cache
+
+ location.reload(true);
+
+ }
+
+ };
+
+
+
+ hmi_hash_u8 = new Uint8Array(hmi_hash);
+
+
+
+ function send_blob(data) {
+
+ if(data.length > 0) {
+
+ ws.send(new Blob([hmi_hash_u8].concat(data)));
+
+ };
+
+ };
+
+
+
+ const typedarray_types = {
+
+ INT: (number) => new Int16Array([number]),
+
+ BOOL: (truth) => new Int16Array([truth]),
+
+ NODE: (truth) => new Int16Array([truth]),
+
+ REAL: (number) => new Float32Array([number]),
+
+ STRING: (str) => {
+
+ // beremiz default string max size is 128
+
+ str = str.slice(0,128);
+
+ binary = new Uint8Array(str.length + 1);
+
+ binary[0] = str.length;
+
+ for(let i = 0; i < str.length; i++){
+
+ binary[i+1] = str.charCodeAt(i);
+
+ }
+
+ return binary;
+
+ }
+
+ /* TODO */
+
+ };
+
+
+
+ function send_reset() {
+
+ send_blob(new Uint8Array([1])); /* reset = 1 */
+
+ };
+
+
+
+ var subscriptions = [];
+
+
+
+ function subscribers(index) {
+
+ let entry = subscriptions[index];
+
+ let res;
+
+ if(entry == undefined){
+
+ res = new Set();
+
+ subscriptions[index] = [res,0];
+
+ }else{
+
+ [res, _ign] = entry;
+
+ }
+
+ return res
+
+ }
+
+
+
+ function get_subscription_period(index) {
+
+ let entry = subscriptions[index];
+
+ if(entry == undefined)
+
+ return 0;
+
+ let [_ign, period] = entry;
+
+ return period;
+
+ }
+
+
+
+ function set_subscription_period(index, period) {
+
+ let entry = subscriptions[index];
+
+ if(entry == undefined){
+
+ subscriptions[index] = [new Set(), period];
+
+ } else {
+
+ entry[1] = period;
+
+ }
+
+ }
+
+
+
+ if(has_watchdog){
+
+ // artificially subscribe the watchdog widget to "/heartbeat" hmi variable
+
+ // Since dispatch directly calls change_hmi_value,
+
+ // PLC will periodically send variable at given frequency
+
+ subscribers(heartbeat_index).add({
+
+ /* type: "Watchdog", */
+
+ frequency: 1,
+
+ indexes: [heartbeat_index],
+
+ new_hmi_value: function(index, value, oldval) {
+
+ apply_hmi_value(heartbeat_index, value+1);
+
+ }
+
+ });
+
+ }
+
+
+
+
+
+ var page_fading = "off";
+
+ var page_fading_args = "off";
+
+ function fading_page_switch(...args){
+
+ if(page_fading == "in_progress")
+
+ page_fading = "forced";
+
+ else
+
+ page_fading = "pending";
+
+ page_fading_args = args;
+
+
+
+ requestHMIAnimation();
+
+
+
+ }
+
+ document.body.style.backgroundColor = "black";
+
+
+
+ // subscribe to per instance current page hmi variable
+
+ // PLC must prefix page name with "!" for page switch to happen
+
+ subscribers(current_page_var_index).add({
+
+ frequency: 1,
+
+ indexes: [current_page_var_index],
+
+ new_hmi_value: function(index, value, oldval) {
+
+ if(value.startsWith("!"))
+
+ fading_page_switch(value.slice(1));
+
+ }
+
+ });
+
+
+
+ function svg_text_to_multiline(elt) {
+
+ return(Array.prototype.map.call(elt.children, x=>x.textContent).join("\n"));
+
+ }
+
+
+
+ function multiline_to_svg_text(elt, str, blank) {
+
+ str.split('\n').map((line,i) => {elt.children[i].textContent = blank?"":line;});
+
+ }
+
+
+
+ function switch_langnum(langnum) {
+
+ langnum = Math.max(0, Math.min(langs.length - 1, langnum));
+
+
+
+ for (let translation of translations) {
+
+ let [objs, msgs] = translation;
+
+ let msg = msgs[langnum];
+
+ for (let obj of objs) {
+
+ multiline_to_svg_text(obj, msg);
+
+ obj.setAttribute("lang",langnum);
+
+ }
+
+ }
+
+ return langnum;
+
+ }
+
+
+
+ // backup original texts
+
+ for (let translation of translations) {
+
+ let [objs, msgs] = translation;
+
+ msgs.unshift(svg_text_to_multiline(objs[0]));
+
+ }
+
+
+
+ var lang_local_index = hmi_local_index("lang");
+
+ var langcode_local_index = hmi_local_index("lang_code");
+
+ var langname_local_index = hmi_local_index("lang_name");
+
+ subscribers(lang_local_index).add({
+
+ indexes: [lang_local_index],
+
+ new_hmi_value: function(index, value, oldval) {
+
+ let current_lang = switch_langnum(value);
+
+ let [langname,langcode] = langs[current_lang];
+
+ apply_hmi_value(langcode_local_index, langcode);
+
+ apply_hmi_value(langname_local_index, langname);
+
+ switch_page();
+
+ }
+
+ });
+
+
+
+ // returns en_US, fr_FR or en_UK depending on selected language
+
+ function get_current_lang_code(){
+
+ return cache[langcode_local_index];
+
+ }
+
+
+
+ function setup_lang(){
+
+ let current_lang = cache[lang_local_index];
+
+ let new_lang = switch_langnum(current_lang);
+
+ if(current_lang != new_lang){
+
+ apply_hmi_value(lang_local_index, new_lang);
+
+ }
+
+ }
+
+
+
+ setup_lang();
+
+
+
+ function update_subscriptions() {
+
+ let delta = [];
+
+ for(let index in subscriptions){
+
+ let widgets = subscribers(index);
+
+
+
+ // periods are in ms
+
+ let previous_period = get_subscription_period(index);
+
+
+
+ // subscribing with a zero period is unsubscribing
+
+ let new_period = 0;
+
+ if(widgets.size > 0) {
+
+ let maxfreq = 0;
+
+ for(let widget of widgets){
+
+ let wf = widget.frequency;
+
+ if(wf != undefined && maxfreq < wf)
+
+ maxfreq = wf;
+
+ }
+
+
+
+ if(maxfreq != 0)
+
+ new_period = 1000/maxfreq;
+
+ }
+
+
+
+ if(previous_period != new_period) {
+
+ set_subscription_period(index, new_period);
+
+ if(index <= last_remote_index){
+
+ delta.push(
+
+ new Uint8Array([2]), /* subscribe = 2 */
+
+ new Uint32Array([index]),
+
+ new Uint16Array([new_period]));
+
+ }
+
+ }
+
+ }
+
+ send_blob(delta);
+
+ };
+
+
+
+ function send_hmi_value(index, value) {
+
+ if(index > last_remote_index){
dispatch_value(index, value);
+
+
+ if(persistent_indexes.has(index)){
+
+ let varname = persistent_indexes.get(index);
+
+ document.cookie = varname+"="+value+"; max-age=3153600000";
+
+ }
+
+
+
+ return;
+
+ }
+
+
+
+ let iectype = hmitree_types[index];
+
+ let tobinary = typedarray_types[iectype];
+
+ send_blob([
+
+ new Uint8Array([0]), /* setval = 0 */
+
+ new Uint32Array([index]),
+
+ tobinary(value)]);
+
+
+
+ // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf
+
+ // cache[index] = value;
+
+ };
+
+
+
+ function apply_hmi_value(index, new_val) {
+
+ // Similarly to previous comment, taking decision to update based
+
+ // on cache content is bad and can lead to inconsistency
+
+ /*let old_val = cache[index];*/
+
+ if(new_val != undefined /*&& old_val != new_val*/)
+
+ send_hmi_value(index, new_val);
+
+ return new_val;
+
+ }
+
+
+
+ const quotes = {"'":null, '"':null};
+
+
+
+ function eval_operation_string(old_val, opstr) {
+
+ let op = opstr[0];
+
+ let given_val;
+
+ if(opstr.length < 2)
+
+ return undefined;
+
+ if(opstr[1] in quotes){
+
+ if(opstr.length < 3)
+
+ return undefined;
+
+ if(opstr[opstr.length-1] == opstr[1]){
+
+ given_val = opstr.slice(2,opstr.length-1);
+
+ }
+
+ } else {
+
+ given_val = Number(opstr.slice(1));
+
+ }
+
+ let new_val;
+
+ switch(op){
+
+ case "=":
+
+ new_val = given_val;
+
+ break;
+
+ case "+":
+
+ new_val = old_val + given_val;
+
+ break;
+
+ case "-":
+
+ new_val = old_val - given_val;
+
+ break;
+
+ case "*":
+
+ new_val = old_val * given_val;
+
+ break;
+
+ case "/":
+
+ new_val = old_val / given_val;
+
+ break;
+
+ }
+
+ return new_val;
+
+ }
+
+
+
+ var current_visible_page;
+
+ var current_subscribed_page;
+
+ var current_page_index;
+
+ var page_node_local_index = hmi_local_index("page_node");
+
+ var page_switch_in_progress = false;
+
+
+
+ function toggleFullscreen() {
+
+ let elem = document.documentElement;
+
+
+
+ if (!document.fullscreenElement) {
+
+ elem.requestFullscreen().catch(err => {
+
+ console.log("Error attempting to enable full-screen mode: "+err.message+" ("+err.name+")");
+
});
- updates.clear();
+ } else {
+
+ document.exitFullscreen();
+
+ }
}
- // Called on requestAnimationFrame, modifies DOM
-
- var requestAnimationFrameID = null;
-
- function animate() {
-
- let rearm = true;
-
- do{
-
- if(page_fading == "pending" || page_fading == "forced"){
-
- if(page_fading == "pending")
-
- svg_root.classList.add("fade-out-page");
-
- page_fading = "in_progress";
-
- if(page_fading_args.length)
-
- setTimeout(function(){
-
- switch_page(...page_fading_args);
-
- },1);
-
- break;
+ function prepare_svg() {
+
+ // prevents context menu from appearing on right click and long touch
+
+ document.body.addEventListener('contextmenu', e => {
+
+ toggleFullscreen();
+
+ e.preventDefault();
+
+ });
+
+
+
+ for(let eltid in detachable_elements){
+
+ let [element,parent] = detachable_elements[eltid];
+
+ parent.removeChild(element);
+
+ }
+
+ };
+
+
+
+ function switch_page(page_name, page_index) {
+
+ if(page_switch_in_progress){
+
+ /* page switch already going */
+
+ /* TODO LOG ERROR */
+
+ return false;
+
+ }
+
+ page_switch_in_progress = true;
+
+
+
+ if(page_name == undefined)
+
+ page_name = current_subscribed_page;
+
+ else if(page_index == undefined){
+
+ [page_name, page_index] = page_name.split('@')
+
+ }
+
+
+
+ let old_desc = page_desc[current_subscribed_page];
+
+ let new_desc = page_desc[page_name];
+
+
+
+ if(new_desc == undefined){
+
+ /* TODO LOG ERROR */
+
+ return false;
+
+ }
+
+
+
+ if(page_index == undefined)
+
+ page_index = new_desc.page_index;
+
+ else if(typeof(page_index) == "string") {
+
+ let hmitree_node = hmitree_nodes[page_index];
+
+ if(hmitree_node !== undefined){
+
+ let [int_index, hmiclass] = hmitree_node;
+
+ if(hmiclass == new_desc.page_class)
+
+ page_index = int_index;
+
+ else
+
+ page_index = new_desc.page_index;
+
+ } else {
+
+ page_index = new_desc.page_index;
}
-
-
- // Do the page swith if pending
-
- if(page_switch_in_progress){
-
- if(current_subscribed_page != current_visible_page){
-
- switch_visible_page(current_subscribed_page);
+ }
+
+
+
+ if(old_desc){
+
+ old_desc.widgets.map(([widget,relativeness])=>widget.unsub());
+
+ }
+
+ const new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index;
+
+
+
+ const container_id = page_name + (page_index != undefined ? page_index : "");
+
+
+
+ new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness,container_id));
+
+
+
+ update_subscriptions();
+
+
+
+ current_subscribed_page = page_name;
+
+ current_page_index = page_index;
+
+ let page_node;
+
+ if(page_index != undefined){
+
+ page_node = hmitree_paths[page_index];
+
+ }else{
+
+ page_node = "";
+
+ }
+
+ apply_hmi_value(page_node_local_index, page_node);
+
+
+
+ jumps_need_update = true;
+
+
+
+ requestHMIAnimation();
+
+ jump_history.push([page_name, page_index]);
+
+ if(jump_history.length > 42)
+
+ jump_history.shift();
+
+
+
+ apply_hmi_value(current_page_var_index, page_index == undefined
+
+ ? page_name
+
+ : page_name + "@" + hmitree_paths[page_index]);
+
+
+
+ return true;
+
+ };
+
+
+
+ function switch_visible_page(page_name) {
+
+
+
+ let old_desc = page_desc[current_visible_page];
+
+ let new_desc = page_desc[page_name];
+
+
+
+ if(old_desc){
+
+ for(let eltid in old_desc.required_detachables){
+
+ if(!(eltid in new_desc.required_detachables)){
+
+ let [element, parent] = old_desc.required_detachables[eltid];
+
+ parent.removeChild(element);
}
-
-
- page_switch_in_progress = false;
-
-
-
- if(page_fading == "in_progress"){
-
- svg_root.classList.remove("fade-out-page");
-
- page_fading = "off";
+ }
+
+ for(let eltid in new_desc.required_detachables){
+
+ if(!(eltid in old_desc.required_detachables)){
+
+ let [element, parent] = new_desc.required_detachables[eltid];
+
+ parent.appendChild(element);
}
}
-
-
- if(jumps_need_update) update_jumps();
-
-
-
-
-
- pending_widget_animates.forEach(widget => widget._animate());
-
- pending_widget_animates = [];
-
- rearm = false;
-
- } while(0);
-
-
-
- requestAnimationFrameID = null;
-
-
-
- if(rearm) requestHMIAnimation();
+ }else{
+
+ for(let eltid in new_desc.required_detachables){
+
+ let [element, parent] = new_desc.required_detachables[eltid];
+
+ parent.appendChild(element);
+
+ }
+
+ }
+
+
+
+ svg_root.setAttribute('viewBox',new_desc.bbox.join(" "));
+
+ current_visible_page = page_name;
+
+ };
+
+
+
+ /* From https://jsfiddle.net/ibowankenobi/1mmh7rs6/6/ */
+
+ function getAbsoluteCTM(element){
+
+ var height = svg_root.height.baseVal.value,
+
+ width = svg_root.width.baseVal.value,
+
+ viewBoxRect = svg_root.viewBox.baseVal,
+
+ vHeight = viewBoxRect.height,
+
+ vWidth = viewBoxRect.width;
+
+ if(!vWidth || !vHeight){
+
+ return element.getCTM();
+
+ }
+
+ var sH = height/vHeight,
+
+ sW = width/vWidth,
+
+ matrix = svg_root.createSVGMatrix();
+
+ matrix.a = sW;
+
+ matrix.d = sH
+
+ var realCTM = element.getCTM().multiply(matrix.inverse());
+
+ realCTM.e = realCTM.e/sW + viewBoxRect.x;
+
+ realCTM.f = realCTM.f/sH + viewBoxRect.y;
+
+ return realCTM;
}
- function requestHMIAnimation() {
-
- if(requestAnimationFrameID == null){
-
- requestAnimationFrameID = window.requestAnimationFrame(animate);
-
- }
+ function apply_reference_frames(){
+
+ const matches = svg_root.querySelectorAll("g[svghmi_x_offset]");
+
+ matches.forEach((group) => {
+
+ let [x,y] = ["x", "y"].map((axis) => Number(group.getAttribute("svghmi_"+axis+"_offset")));
+
+ let ctm = getAbsoluteCTM(group);
+
+ // zero translation part of CTM
+
+ // to only apply rotation/skewing to offset vector
+
+ ctm.e = 0;
+
+ ctm.f = 0;
+
+ let invctm = ctm.inverse();
+
+ let vect = new DOMPoint(x, y);
+
+ let newvect = vect.matrixTransform(invctm);
+
+ let transform = svg_root.createSVGTransform();
+
+ transform.setTranslate(newvect.x, newvect.y);
+
+ group.transform.baseVal.appendItem(transform);
+
+ ["x", "y"].forEach((axis) => group.removeAttribute("svghmi_"+axis+"_offset"));
+
+ });
}
- // Message reception handler
-
- // Hash is verified and HMI values updates resulting from binary parsing
-
- // are stored until browser can compute next frame, DOM is left untouched
-
- ws.onmessage = function (evt) {
-
-
-
- let data = evt.data;
-
- let dv = new DataView(data);
-
- let i = 0;
-
- try {
-
- for(let hash_int of hmi_hash) {
-
- if(hash_int != dv.getUint8(i)){
-
- throw new Error("Hash doesn't match");
-
- };
-
- i++;
-
- };
-
-
-
- while(i < data.byteLength){
-
- let index = dv.getUint32(i, true);
-
- i += 4;
-
- let iectype = hmitree_types[index];
-
- if(iectype != undefined){
-
- let dvgetter = dvgetters[iectype];
-
- let [value, bytesize] = dvgetter(dv,i);
-
- updates.set(index, value);
-
- i += bytesize;
-
- } else {
-
- throw new Error("Unknown index "+index);
-
- }
-
- };
-
-
-
- apply_updates();
-
- // register for rendering on next frame, since there are updates
-
- } catch(err) {
-
- // 1003 is for "Unsupported Data"
-
- // ws.close(1003, err.message);
-
-
-
- // TODO : remove debug alert ?
-
- alert("Error : "+err.message+"\nHMI will be reloaded.");
-
-
-
- // force reload ignoring cache
-
- location.reload(true);
-
- }
-
- };
-
-
-
- hmi_hash_u8 = new Uint8Array(hmi_hash);
-
-
-
- function send_blob(data) {
-
- if(data.length > 0) {
-
- ws.send(new Blob([hmi_hash_u8].concat(data)));
-
- };
-
- };
-
-
-
- const typedarray_types = {
-
- INT: (number) => new Int16Array([number]),
-
- BOOL: (truth) => new Int16Array([truth]),
-
- NODE: (truth) => new Int16Array([truth]),
-
- REAL: (number) => new Float32Array([number]),
-
- STRING: (str) => {
-
- // beremiz default string max size is 128
-
- str = str.slice(0,128);
-
- binary = new Uint8Array(str.length + 1);
-
- binary[0] = str.length;
-
- for(let i = 0; i < str.length; i++){
-
- binary[i+1] = str.charCodeAt(i);
-
- }
-
- return binary;
-
- }
-
- /* TODO */
-
- };
-
-
-
- function send_reset() {
-
- send_blob(new Uint8Array([1])); /* reset = 1 */
-
- };
-
-
-
- var subscriptions = [];
-
-
-
- function subscribers(index) {
-
- let entry = subscriptions[index];
-
- let res;
-
- if(entry == undefined){
-
- res = new Set();
-
- subscriptions[index] = [res,0];
-
- }else{
-
- [res, _ign] = entry;
-
- }
-
- return res
-
- }
-
-
-
- function get_subscription_period(index) {
-
- let entry = subscriptions[index];
-
- if(entry == undefined)
-
- return 0;
-
- let [_ign, period] = entry;
-
- return period;
-
- }
-
-
-
- function set_subscription_period(index, period) {
-
- let entry = subscriptions[index];
-
- if(entry == undefined){
-
- subscriptions[index] = [new Set(), period];
-
- } else {
-
- entry[1] = period;
-
- }
-
- }
-
-
-
- if(has_watchdog){
-
- // artificially subscribe the watchdog widget to "/heartbeat" hmi variable
-
- // Since dispatch directly calls change_hmi_value,
-
- // PLC will periodically send variable at given frequency
-
- subscribers(heartbeat_index).add({
-
- /* type: "Watchdog", */
-
- frequency: 1,
-
- indexes: [heartbeat_index],
-
- new_hmi_value: function(index, value, oldval) {
-
- apply_hmi_value(heartbeat_index, value+1);
-
- }
-
- });
-
- }
-
-
-
-
-
- var page_fading = "off";
-
- var page_fading_args = "off";
-
- function fading_page_switch(...args){
-
- if(page_fading == "in_progress")
-
- page_fading = "forced";
-
- else
-
- page_fading = "pending";
-
- page_fading_args = args;
-
-
-
- requestHMIAnimation();
-
-
-
- }
-
- document.body.style.backgroundColor = "black";
-
-
-
- // subscribe to per instance current page hmi variable
-
- // PLC must prefix page name with "!" for page switch to happen
-
- subscribers(current_page_var_index).add({
-
- frequency: 1,
-
- indexes: [current_page_var_index],
-
- new_hmi_value: function(index, value, oldval) {
-
- if(value.startsWith("!"))
-
- fading_page_switch(value.slice(1));
-
- }
-
- });
-
-
-
- function svg_text_to_multiline(elt) {
-
- return(Array.prototype.map.call(elt.children, x=>x.textContent).join("\n"));
-
- }
-
-
-
- function multiline_to_svg_text(elt, str, blank) {
-
- str.split('\n').map((line,i) => {elt.children[i].textContent = blank?"":line;});
-
- }
-
-
-
- function switch_langnum(langnum) {
-
- langnum = Math.max(0, Math.min(langs.length - 1, langnum));
-
-
-
- for (let translation of translations) {
-
- let [objs, msgs] = translation;
-
- let msg = msgs[langnum];
-
- for (let obj of objs) {
-
- multiline_to_svg_text(obj, msg);
-
- obj.setAttribute("lang",langnum);
-
- }
-
- }
-
- return langnum;
-
- }
-
-
-
- // backup original texts
-
- for (let translation of translations) {
-
- let [objs, msgs] = translation;
-
- msgs.unshift(svg_text_to_multiline(objs[0]));
-
- }
-
-
-
- var lang_local_index = hmi_local_index("lang");
-
- var langcode_local_index = hmi_local_index("lang_code");
-
- var langname_local_index = hmi_local_index("lang_name");
-
- subscribers(lang_local_index).add({
-
- indexes: [lang_local_index],
-
- new_hmi_value: function(index, value, oldval) {
-
- let current_lang = switch_langnum(value);
-
- let [langname,langcode] = langs[current_lang];
-
- apply_hmi_value(langcode_local_index, langcode);
-
- apply_hmi_value(langname_local_index, langname);
-
- switch_page();
-
- }
-
- });
-
-
-
- // returns en_US, fr_FR or en_UK depending on selected language
-
- function get_current_lang_code(){
-
- return cache[langcode_local_index];
-
- }
-
-
-
- function setup_lang(){
-
- let current_lang = cache[lang_local_index];
-
- let new_lang = switch_langnum(current_lang);
-
- if(current_lang != new_lang){
-
- apply_hmi_value(lang_local_index, new_lang);
-
- }
-
- }
-
-
-
- setup_lang();
-
-
-
- function update_subscriptions() {
-
- let delta = [];
-
- for(let index in subscriptions){
-
- let widgets = subscribers(index);
-
-
-
- // periods are in ms
-
- let previous_period = get_subscription_period(index);
-
-
-
- // subscribing with a zero period is unsubscribing
-
- let new_period = 0;
-
- if(widgets.size > 0) {
-
- let maxfreq = 0;
-
- for(let widget of widgets){
-
- let wf = widget.frequency;
-
- if(wf != undefined && maxfreq < wf)
-
- maxfreq = wf;
-
- }
-
-
-
- if(maxfreq != 0)
-
- new_period = 1000/maxfreq;
-
- }
-
-
-
- if(previous_period != new_period) {
-
- set_subscription_period(index, new_period);
-
- if(index <= last_remote_index){
-
- delta.push(
-
- new Uint8Array([2]), /* subscribe = 2 */
-
- new Uint32Array([index]),
-
- new Uint16Array([new_period]));
-
- }
-
- }
-
- }
-
- send_blob(delta);
-
- };
-
-
-
- function send_hmi_value(index, value) {
-
- if(index > last_remote_index){
-
- dispatch_value(index, value);
-
-
-
- if(persistent_indexes.has(index)){
-
- let varname = persistent_indexes.get(index);
-
- document.cookie = varname+"="+value+"; max-age=3153600000";
-
- }
-
-
-
- return;
-
- }
-
-
-
- let iectype = hmitree_types[index];
-
- let tobinary = typedarray_types[iectype];
-
- send_blob([
-
- new Uint8Array([0]), /* setval = 0 */
-
- new Uint32Array([index]),
-
- tobinary(value)]);
-
-
-
- // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf
-
- // cache[index] = value;
-
- };
-
-
-
- function apply_hmi_value(index, new_val) {
-
- // Similarly to previous comment, taking decision to update based
-
- // on cache content is bad and can lead to inconsistency
-
- /*let old_val = cache[index];*/
-
- if(new_val != undefined /*&& old_val != new_val*/)
-
- send_hmi_value(index, new_val);
-
- return new_val;
-
- }
-
-
-
- const quotes = {"'":null, '"':null};
-
-
-
- function eval_operation_string(old_val, opstr) {
-
- let op = opstr[0];
-
- let given_val;
-
- if(opstr.length < 2)
-
- return undefined;
-
- if(opstr[1] in quotes){
-
- if(opstr.length < 3)
-
- return undefined;
-
- if(opstr[opstr.length-1] == opstr[1]){
-
- given_val = opstr.slice(2,opstr.length-1);
-
- }
-
- } else {
-
- given_val = Number(opstr.slice(1));
-
- }
-
- let new_val;
-
- switch(op){
-
- case "=":
-
- new_val = given_val;
-
- break;
-
- case "+":
-
- new_val = old_val + given_val;
-
- break;
-
- case "-":
-
- new_val = old_val - given_val;
-
- break;
-
- case "*":
-
- new_val = old_val * given_val;
-
- break;
-
- case "/":
-
- new_val = old_val / given_val;
-
- break;
-
- }
-
- return new_val;
-
- }
-
-
-
- var current_visible_page;
-
- var current_subscribed_page;
-
- var current_page_index;
-
- var page_node_local_index = hmi_local_index("page_node");
-
- var page_switch_in_progress = false;
-
-
-
- function toggleFullscreen() {
-
- let elem = document.documentElement;
-
-
-
- if (!document.fullscreenElement) {
-
- elem.requestFullscreen().catch(err => {
-
- console.log("Error attempting to enable full-screen mode: "+err.message+" ("+err.name+")");
-
- });
-
- } else {
-
- document.exitFullscreen();
-
- }
-
- }
-
-
-
- function prepare_svg() {
-
- // prevents context menu from appearing on right click and long touch
-
- document.body.addEventListener('contextmenu', e => {
-
- toggleFullscreen();
-
- e.preventDefault();
-
- });
-
-
-
- for(let eltid in detachable_elements){
-
- let [element,parent] = detachable_elements[eltid];
-
- parent.removeChild(element);
-
- }
-
- };
-
-
-
- function switch_page(page_name, page_index) {
-
- if(page_switch_in_progress){
-
- /* page switch already going */
-
- /* TODO LOG ERROR */
-
- return false;
-
- }
-
- page_switch_in_progress = true;
-
-
-
- if(page_name == undefined)
-
- page_name = current_subscribed_page;
-
- else if(page_index == undefined){
-
- [page_name, page_index] = page_name.split('@')
-
- }
-
-
-
- let old_desc = page_desc[current_subscribed_page];
-
- let new_desc = page_desc[page_name];
-
-
-
- if(new_desc == undefined){
-
- /* TODO LOG ERROR */
-
- return false;
-
- }
-
-
-
- if(page_index == undefined)
-
- page_index = new_desc.page_index;
-
- else if(typeof(page_index) == "string") {
-
- let hmitree_node = hmitree_nodes[page_index];
-
- if(hmitree_node !== undefined){
-
- let [int_index, hmiclass] = hmitree_node;
-
- if(hmiclass == new_desc.page_class)
-
- page_index = int_index;
-
- else
-
- page_index = new_desc.page_index;
-
- } else {
-
- page_index = new_desc.page_index;
-
- }
-
- }
-
-
-
- if(old_desc){
-
- old_desc.widgets.map(([widget,relativeness])=>widget.unsub());
-
- }
-
- const new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index;
-
-
-
- const container_id = page_name + (page_index != undefined ? page_index : "");
-
-
-
- new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness,container_id));
-
-
-
- update_subscriptions();
-
-
-
- current_subscribed_page = page_name;
-
- current_page_index = page_index;
-
- let page_node;
-
- if(page_index != undefined){
-
- page_node = hmitree_paths[page_index];
-
- }else{
-
- page_node = "";
-
- }
-
- apply_hmi_value(page_node_local_index, page_node);
-
-
-
- jumps_need_update = true;
-
-
-
- requestHMIAnimation();
-
- jump_history.push([page_name, page_index]);
-
- if(jump_history.length > 42)
-
- jump_history.shift();
-
-
-
- apply_hmi_value(current_page_var_index, page_index == undefined
-
- ? page_name
-
- : page_name + "@" + hmitree_paths[page_index]);
-
-
-
- return true;
-
- };
-
-
-
- function switch_visible_page(page_name) {
-
-
-
- let old_desc = page_desc[current_visible_page];
-
- let new_desc = page_desc[page_name];
-
-
-
- if(old_desc){
-
- for(let eltid in old_desc.required_detachables){
-
- if(!(eltid in new_desc.required_detachables)){
-
- let [element, parent] = old_desc.required_detachables[eltid];
-
- parent.removeChild(element);
-
- }
-
- }
-
- for(let eltid in new_desc.required_detachables){
-
- if(!(eltid in old_desc.required_detachables)){
-
- let [element, parent] = new_desc.required_detachables[eltid];
-
- parent.appendChild(element);
-
- }
-
- }
-
- }else{
-
- for(let eltid in new_desc.required_detachables){
-
- let [element, parent] = new_desc.required_detachables[eltid];
-
- parent.appendChild(element);
-
- }
-
- }
-
-
-
- svg_root.setAttribute('viewBox',new_desc.bbox.join(" "));
-
- current_visible_page = page_name;
-
- };
-
-
-
// Once connection established
ws.onopen = function (evt) {
+ apply_reference_frames();
+
init_widgets();
send_reset();