svghmi/widgets_common.ysl2
author Edouard Tisserant
Wed, 10 Mar 2021 09:59:18 +0100
branchsvghmi
changeset 3182 a7941f053a83
parent 3162 fbe1ffaa56cf
child 3232 7bdb766c2a4d
permissions -rw-r--r--
Runtime: Added PostStart methot to PLCObject, called after PLC is started, with all libraries and python object already created, and python extensions "Start" methods being called.
This is called before python thread processing py_eval blocks starts. Example purpose: attach additional ressource to web services
// 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;
    }
};

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";
        }
    }
}