// widgets_common.ysl2 in xsl decl labels(*ptr, name="defs_by_labels") alias call-template { with "hmi_element", "$hmi_element"; with "labels"{text *ptr}; content; }; decl optional_labels(*ptr) alias - { /* TODO add some per label xslt variable to check if exist */ labels(*ptr){ with "mandatory","'no'"; content; } }; decl warning_labels(*ptr) alias - { labels(*ptr){ with "mandatory","'warn'"; content; } }; decl _activable(*level) alias - { | activable_sub:{ const "activity" labels("/active /inactive") { with "mandatory"{text *level}; content; } value "$activity"; const "has_activity","string-length($activity)>0"; | }, | has_activity: «$has_activity», }; decl activable() alias - { _activable("warn") }; decl optional_activable() alias - { _activable("no") }; decl activable_labels(*ptr) alias - { optional_labels(*ptr) { with "subelements","'active inactive'"; content; } }; in xsl decl widget_desc(%name, match="widget[@type='%name']", mode="widget_desc") alias template { type > «@type» content; }; in xsl decl widget_class(%name, *clsname="%nameWidget", match="widget[@type='%name']", mode="widget_class") alias template { | class `text **clsname` extends Widget{ content; | } }; in xsl decl widget_defs(%name, match="widget[@type='%name']", mode="widget_defs") alias template { param "hmi_element"; // all widget potentially has a "disabled" labeled element const "disability" optional_labels("/disabled"); value "$disability"; const "has_disability","string-length($disability)>0"; content; }; in xsl decl widget_page(%name, match="widget[@type='%name']", mode="widget_page") alias template { param "page_desc"; content; }; decl gen_index_xhtml alias - { content; }; template "svg:*", mode="hmi_widgets" { const "widget", "func:widget(@id)"; const "eltid","@id"; const "args" foreach "$widget/arg" > "«func:escape_quotes(@value)»"`if "position()!=last()" > ,` const "indexes" foreach "$widget/path" { if "position()!=last()" > , } const "variables" foreach "$widget/path" { > [ choose { when "not(@index)" { choose { when "not(@type)" { warning > Widget «$widget/@type» id="«$eltid»" : No match for path "«@value»" in HMI tree > undefined } when "@type = 'PAGE_LOCAL'" > "«@value»" when "@type = 'HMI_LOCAL'" > hmi_local_index("«@value»") otherwise error > Internal error while processing widget's non indexed HMI tree path : unknown type } } otherwise { > «@index» } } > , { if "@min and @max"{ > minmax:[«@min», «@max»] if "@assign" > , } if "@assign" > assign:"«@assign»" > }] if "position()!=last()" > , } const "freq" choose { when "$widget/@freq" > "«$widget/@freq»" otherwise > undefined } const "enable_expr" choose{ when "$widget/@enable_expr" > true otherwise > false } | "«@id»": new «$widget/@type»Widget ("«@id»",«$freq»,[«$args»],[«$variables»],«$enable_expr»,{ if "$widget/@enable_expr" { | enable_assignments: [], | compute_enable: function(value, oldval, varnum) { | let result = false; | do { foreach "$widget/path" { const "varid","generate-id()"; const "varnum","position()-1"; if "@assign" foreach "$widget/path[@assign]" if "$varid = generate-id()" { | if(varnum == «$varnum») this.enable_assignments[«position()-1»] = value; | let «@assign» = this.enable_assignments[«position()-1»]; | if(«@assign» == undefined) break; } } | result = «$widget/@enable_expr»; | } while(0); | this.enable(result); | }, } apply "$widget", mode="widget_defs" with "hmi_element","."; | })`if "position()!=last()" > ,` } emit "preamble:local-variable-indexes" { || let hmi_locals = {}; var last_remote_index = hmitree_types.length - 1; var next_available_index = hmitree_types.length; let cookies = new Map(document.cookie.split("; ").map(s=>s.split("="))); const local_defaults = { || foreach "$parsed_widgets/widget[starts-with(@type,'VarInit')]"{ if "count(path) != 1" error > VarInit «@id» must have only one variable given. if "path/@type != 'PAGE_LOCAL' and path/@type != 'HMI_LOCAL'" error > VarInit «@id» only applies to HMI variable. > "«path/@value»": choose { when "@type = 'VarInitPersistent'" > cookies.has("«path/@value»")?cookies.get("«path/@value»"):«arg[1]/@value» otherwise > «arg[1]/@value» } > \n if "position()!=last()" > , } || }; const persistent_locals = new Set([ || foreach "$parsed_widgets/widget[@type='VarInitPersistent']"{ | "«path/@value»"`if "position()!=last()" > ,` } || ]); var persistent_indexes = new Map(); var cache = hmitree_types.map(_ignored => undefined); function page_local_index(varname, pagename){ let pagevars = hmi_locals[pagename]; let new_index; if(pagevars == undefined){ new_index = next_available_index++; hmi_locals[pagename] = {[varname]:new_index}; } else { let result = pagevars[varname]; if(result != undefined) { return result; } new_index = next_available_index++; pagevars[varname] = new_index; } let defaultval = local_defaults[varname]; if(defaultval != undefined) { cache[new_index] = defaultval; if(persistent_locals.has(varname)) persistent_indexes.set(new_index, varname); } return new_index; } function hmi_local_index(varname){ return page_local_index(varname, "HMI_LOCAL"); } || } emit "preamble:widget-base-class" { || var pending_widget_animates = []; function _hide(elt, placeholder){ if(elt.parentNode != null) placeholder.parentNode.removeChild(elt); } function _show(elt, placeholder){ placeholder.parentNode.insertBefore(elt, placeholder); } function set_activity_state(eltsub, state){ if(eltsub.active_elt != undefined){ if(eltsub.active_elt_placeholder == undefined){ eltsub.active_elt_placeholder = document.createComment(""); eltsub.active_elt.parentNode.insertBefore(eltsub.active_elt_placeholder, eltsub.active_elt); } (state?_show:_hide)(eltsub.active_elt, eltsub.active_elt_placeholder); } if(eltsub.inactive_elt != undefined){ if(eltsub.inactive_elt_placeholder == undefined){ eltsub.inactive_elt_placeholder = document.createComment(""); eltsub.inactive_elt.parentNode.insertBefore(eltsub.inactive_elt_placeholder, eltsub.inactive_elt); } ((state || state==undefined)?_hide:_show)(eltsub.inactive_elt, eltsub.inactive_elt_placeholder); } } class Widget { offset = 0; frequency = 10; /* FIXME arbitrary default max freq. Obtain from config ? */ unsubscribable = false; pending_animate = false; constructor(elt_id, freq, args, variables, enable_expr, members){ this.element_id = elt_id; this.element = id(elt_id); this.args = args; [this.indexes, this.variables_options] = (variables.length>0) ? zip(...variables) : [[],[]]; this.indexes_length = this.indexes.length; this.enable_expr = enable_expr; this.enable_state = true; this.enable_displayed_state = true; this.enabled_elts = []; Object.keys(members).forEach(prop => this[prop]=members[prop]); this.lastapply = this.indexes.map(() => undefined); this.inhibit = this.indexes.map(() => undefined); this.pending = this.indexes.map(() => undefined); this.bound_uninhibit = this.uninhibit.bind(this); this.lastdispatch = this.indexes.map(() => undefined); this.deafen = this.indexes.map(() => undefined); this.incoming = this.indexes.map(() => undefined); this.bound_undeafen = this.undeafen.bind(this); this.forced_frequency = freq; this.clip = true; } do_init(){ let forced = this.forced_frequency; if(forced !== undefined){ /* once every 10 seconds : 10s once per minute : 1m once per hour : 1h once per day : 1d */ let unit = forced.slice(-1); let factor = { "s":1, "m":60, "h":3600, "d":86400}[unit]; this.frequency = factor ? 1/(factor * Number(forced.slice(0,-1))) : Number(forced); } let init = this.init; if(typeof(init) == "function"){ try { init.call(this); } catch(err) { console.log(err); } } if(this.enable_expr){ this.enable_state = false; this.enable_displayed_state = false; for(let child of Array.from(this.element.children)){ let label = child.getAttribute("inkscape:label"); if(label!="disabled"){ this.enabled_elts.push(child); this.element.removeChild(child); } } } } unsub(){ /* remove subsribers */ for(let i = 0; i < this.indexes_length; i++) { /* flush updates pending because of inhibition */ let inhibition = this.inhibit[i]; if(inhibition != undefined){ clearTimeout(inhibition); this.lastapply[i] = undefined; this.uninhibit(i); } let deafened = this.deafen[i]; if(deafened != undefined){ clearTimeout(deafened); this.lastdispatch[i] = undefined; this.undeafen(i); } let index = this.get_variable_index(i); subscribers(index).delete(this); } this.offset = 0; this.relativeness = undefined; } sub(new_offset, relativeness, container_id){ this.offset = new_offset; this.relativeness = relativeness; this.container_id = container_id ; /* add this's subsribers */ for(let i = 0; i < this.indexes_length; i++) { let index = this.get_variable_index(i); if(index == undefined) continue; subscribers(index).add(this); } this.apply_cache(); } apply_cache() { for(let i = 0; i < this.indexes_length; i++) { /* dispatch current cache in newly opened page widgets */ let realindex = this.get_variable_index(i); if(realindex == undefined) continue; let cached_val = cache[realindex]; if(cached_val != undefined) this.feed_data_for_dispatch(cached_val, cached_val, i); } } get_variable_index(varnum) { let index = this.indexes[varnum]; if(typeof(index) == "string"){ index = page_local_index(index, this.container_id); } else { if(this.relativeness[varnum]){ index += this.offset; } } return index; } overshot(new_val, max) { } undershot(new_val, min) { } clip_min_max(index, new_val) { let minmax = this.variables_options[index].minmax; if(minmax !== undefined && typeof new_val == "number") { let [min,max] = minmax; if(new_val < min){ this.undershot(new_val, min); return min; } if(new_val > max){ this.overshot(new_val, max); return max; } } return new_val; } change_hmi_value(index, opstr) { let realindex = this.get_variable_index(index); if(realindex == undefined) return undefined; let old_val = cache[realindex]; let new_val = eval_operation_string(old_val, opstr); if(this.clip) new_val = this.clip_min_max(index, new_val); return apply_hmi_value(realindex, new_val); } _apply_hmi_value(index, new_val) { let realindex = this.get_variable_index(index); if(realindex == undefined) return undefined; if(this.clip) new_val = this.clip_min_max(index, new_val); return apply_hmi_value(realindex, new_val); } uninhibit(index){ this.inhibit[index] = undefined; let new_val = this.pending[index]; this.pending[index] = undefined; return this.apply_hmi_value(index, new_val); } apply_hmi_value(index, new_val) { if(this.inhibit[index] == undefined){ let now = Date.now(); let min_interval = 1000/this.frequency; let lastapply = this.lastapply[index]; if(lastapply == undefined || now > lastapply + min_interval){ this.lastapply[index] = now; return this._apply_hmi_value(index, new_val); } else { let elapsed = now - lastapply; this.pending[index] = new_val; this.inhibit[index] = setTimeout(this.bound_uninhibit, min_interval - elapsed, index); } } else { this.pending[index] = new_val; return new_val; } } new_hmi_value(index, value, oldval) { // TODO avoid searching, store index at sub() for(let i = 0; i < this.indexes_length; i++) { let refindex = this.get_variable_index(i); if(refindex == undefined) continue; if(index == refindex) { this.feed_data_for_dispatch(value, oldval, i); break; } } } undeafen(index){ this.deafen[index] = undefined; let [new_val, old_val] = this.incoming[index]; this.incoming[index] = undefined; this.lastdispatch[index] = Date.now(); this.do_dispatch(new_val, old_val, index); } enable(enabled){ if(this.enable_state != enabled){ this.enable_state = enabled; this.request_animate(); } } animate_enable(){ if(this.enable_state && !this.enable_displayed_state){ //show widget for(let child of this.enabled_elts){ this.element.appendChild(child); } //hide disabled content if(this.disabled_elt && this.disabled_elt.parentNode != null) this.element.removeChild(this.disabled_elt); this.enable_displayed_state = true; }else if(!this.enable_state && this.enable_displayed_state){ //hide widget for(let child of this.enabled_elts){ if(child.parentNode != null) this.element.removeChild(child); } //show disabled content if(this.disabled_elt) this.element.appendChild(this.disabled_elt); this.enable_displayed_state = false; // once disabled activity display is lost this.activity_displayed_state = undefined; } } feed_data_for_dispatch(value, oldval, varnum) { if(this.dispatch || this.enable_expr){ if(this.deafen[varnum] == undefined){ let now = Date.now(); let min_interval = 1000/this.frequency; let lastdispatch = this.lastdispatch[varnum]; if(lastdispatch == undefined || now > lastdispatch + min_interval){ this.lastdispatch[varnum] = now; this.do_dispatch(value, oldval, varnum) } else { let elapsed = now - lastdispatch; this.incoming[varnum] = [value, oldval]; this.deafen[varnum] = setTimeout(this.bound_undeafen, min_interval - elapsed, varnum); } } else { this.incoming[varnum] = [value, oldval]; } } } do_dispatch(value, oldval, varnum) { if(this.dispatch) try { this.dispatch(value, oldval, varnum); } catch(err) { console.log(err); } if(this.enable_expr) try { this.compute_enable(value, oldval, varnum); } catch(err) { console.log(err); } } _animate(){ if(this.enable_expr) this.animate_enable(); // inhibit widget animation when disabled if(!this.enable_expr || this.enable_state){ if(this.has_activity) this.animate_activity(); if(this.animate != undefined) this.animate(); } this.pending_animate = false; } request_animate(){ if(!this.pending_animate){ pending_widget_animates.push(this); this.pending_animate = true; requestHMIAnimation(); } } animate_activity(){ if(this.activity_displayed_state != this.activity_state){ set_activity_state(this.activable_sub, this.activity_state); this.activity_displayed_state = this.activity_state; } } } || } const "excluded_types", "str:split('Page VarInit VarInitPersistent')"; // Key to filter unique types key "TypesKey", "widget", "@type"; emit "declarations:hmi-classes" { const "used_widget_types", """$parsed_widgets/widget[ generate-id() = generate-id(key('TypesKey', @type)) and not(@type = $excluded_types)]"""; apply "$used_widget_types", mode="widget_class"; } template "widget", mode="widget_class" { || class «@type»Widget extends Widget{ /* empty class, as «@type» widget didn't provide any */ } || warning > «@type» widget is used in SVG but widget type is not declared } const "included_ids","$parsed_widgets/widget[not(@type = $excluded_types) and not(@id = $discardable_elements/@id)]/@id"; const "page_ids","$parsed_widgets/widget[@type = 'Page']/@id"; const "hmi_widgets","$hmi_elements[@id = $included_ids]"; const "page_widgets","$hmi_elements[@id = $page_ids]"; const "result_widgets","$result_svg_ns//*[@id = $hmi_widgets/@id]"; emit "declarations:hmi-elements" { | var hmi_widgets = { apply "$hmi_widgets | $page_widgets", mode="hmi_widgets"; | } | } function "defs_by_labels" { param "labels","''"; param "mandatory","'yes'"; param "subelements","/.."; param "hmi_element"; const "widget_type","@type"; const "widget_id","@id"; foreach "str:split($labels)" { const "absolute", "starts-with(., '/')"; const "name","substring(.,number($absolute)+1)"; const "widget","$result_widgets[@id = $hmi_element/@id]"; const "elt","($widget//*[not($absolute) and @inkscape:label=$name] | $widget/*[$absolute and @inkscape:label=$name])[1]"; choose { when "not($elt/@id)" { if "$mandatory!='no'" { const "errmsg" > «$widget_type» widget (id=«$widget_id») must have a «$name» element choose { when "$mandatory='yes'" { error > «$errmsg» } otherwise { warning > «$errmsg» } } } // otherwise produce nothing } otherwise { | «$name»_elt: id("«$elt/@id»"), if "$subelements" { | «$name»_sub: { foreach "str:split($subelements)" { const "subname","."; const "subelt","$elt/*[@inkscape:label=$subname][1]"; choose { when "not($subelt/@id)" { if "$mandatory!='no'" { const "errmsg" > «$widget_type» widget (id=«$widget_id») must have a «$name»/«$subname» element choose { when "$mandatory='yes'" { error > «$errmsg» } otherwise { warning > «$errmsg» } } } | /* missing «$name»/«$subname» element */ } otherwise { | "«$subname»_elt": id("«$subelt/@id»")`if "position()!=last()" > ,` } } } | }, } } } } } def "func:escape_quotes" { param "txt"; // have to use a python string to enter escaped quote // const "frstln", "string-length($frst)"; choose { when !"contains($txt,'\"')"! { result !"concat(substring-before($txt,'\"'),'\\\"',func:escape_quotes(substring-after($txt,'\"')))"!; } otherwise { result "$txt"; } } }