svghmi/widgets_common.ysl2
changeset 3302 c89fc366bebd
parent 3233 315f17e74ef5
child 3408 13c5cac55ac7
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/widgets_common.ysl2	Thu Sep 02 21:36:29 2021 +0200
@@ -0,0 +1,438 @@
+// 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 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";
+    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" {
+        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 "position()!=last()" > ,
+    }
+
+    const "minmaxes" foreach "$widget/path" {
+        choose {
+            when "@min and @max"
+                > [«@min»,«@max»]
+            otherwise
+                > undefined
+        }
+        if "position()!=last()" > ,
+    }
+
+    |   "«@id»": new «$widget/@type»Widget ("«@id»",[«$args»],[«$indexes»],[«$minmaxes»],{
+    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);
+    var updates = new Map();
+
+    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; 
+            updates.set(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 = [];
+
+    class Widget {
+        offset = 0;
+        frequency = 10; /* FIXME arbitrary default max freq. Obtain from config ? */
+        unsubscribable = false;
+        pending_animate = false;
+
+        constructor(elt_id,args,indexes,minmaxes,members){
+            this.element_id = elt_id;
+            this.element = id(elt_id);
+            this.args = args;
+            this.indexes = indexes;
+            this.minmaxes = minmaxes;
+            Object.keys(members).forEach(prop => this[prop]=members[prop]);
+            this.lastapply = indexes.map(() => undefined);
+            this.inhibit = indexes.map(() => undefined);
+            this.pending = indexes.map(() => undefined);
+            this.bound_unhinibit = this.unhinibit.bind(this);
+        }
+
+        unsub(){
+            /* remove subsribers */
+            if(!this.unsubscribable)
+                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.unhinibit(i);
+                    }
+                    let index = this.indexes[i];
+                    if(this.relativeness[i])
+                        index += this.offset;
+                    subscribers(index).delete(this);
+                }
+            this.offset = 0;
+            this.relativeness = undefined;
+        }
+
+        sub(new_offset=0, relativeness, container_id){
+            this.offset = new_offset;
+            this.relativeness = relativeness;
+            this.container_id = container_id ;
+            /* add this's subsribers */
+            if(!this.unsubscribable)
+                for(let i = 0; i < this.indexes.length; i++) {
+                    let index = this.get_variable_index(i);
+                    if(index == undefined) continue;
+                    subscribers(index).add(this);
+                }
+            need_cache_apply.push(this); 
+        }
+
+        apply_cache() {
+            if(!this.unsubscribable) for(let index in this.indexes){
+                /* dispatch current cache in newly opened page widgets */
+                let realindex = this.get_variable_index(index);
+                if(realindex == undefined) continue;
+                let cached_val = cache[realindex];
+                if(cached_val != undefined)
+                    this._dispatch(cached_val, cached_val, index);
+            }
+        }
+
+        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.minmaxes[index];
+            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);
+            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;
+            new_val = this.clip_min_max(index, new_val);
+            return apply_hmi_value(realindex, new_val);
+        }
+
+        unhinibit(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_unhinibit, 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._dispatch(value, oldval, i);
+                    break;
+                }
+            }
+        }
+        
+        _dispatch(value, oldval, varnum) {
+            let dispatch = this.dispatch;
+            if(dispatch != undefined){
+                try {
+                    dispatch.call(this, value, oldval, varnum);
+                } catch(err) {
+                    console.log(err);
+                }
+            }
+        }
+
+        _animate(){
+            this.animate();
+            this.pending_animate = false;
+        }
+
+        request_animate(){
+            if(!this.pending_animate){
+                pending_widget_animates.push(this);
+                this.pending_animate = true;
+                requestHMIAnimation();
+            }
+
+        }
+
+        activate_activable(eltsub) {
+            eltsub.inactive.style.display = "none";
+            eltsub.active.style.display = "";
+        }
+
+        inactivate_activable(eltsub) {
+            eltsub.active.style.display = "none";
+            eltsub.inactive.style.display = "";
+        }
+    }
+    ||
+}
+
+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 */
+}
+||
+
+const "included_ids","$parsed_widgets/widget[not(@type = $excluded_types) and not(@id = $discardable_elements/@id)]/@id";
+const "hmi_widgets","$hmi_elements[@id = $included_ids]";
+const "result_widgets","$result_svg_ns//*[@id = $hmi_widgets/@id]";
+
+emit "declarations:hmi-elements" {
+    | var hmi_widgets = {
+    apply "$hmi_widgets", mode="hmi_widgets";
+    | }
+}
+
+function "defs_by_labels" {
+    param "labels","''";
+    param "mandatory","'yes'";
+    param "subelements","/..";
+    param "hmi_element";
+    const "widget_type","@type";
+    foreach "str:split($labels)" {
+        const "name",".";
+        const "elt","$result_widgets[@id = $hmi_element/@id]//*[@inkscape:label=$name][1]";
+        choose {
+            when "not($elt/@id)" {
+                if "$mandatory='yes'" {
+                    error > «$widget_type» widget must have a «$name» element
+                }
+                // 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='yes'" {
+                                    error > «$widget_type» widget must have a «$name»/«$subname» element
+                                }
+                |         /* missing «$name»/«$subname» element */
+                            }
+                            otherwise {
+                |         "«$subname»": 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";
+        }
+    }
+}
+