svghmi/widgets_common.ysl2
author Edouard Tisserant <edouard.tisserant@gmail.com>
Sun, 14 Feb 2021 05:22:30 +0100
branchsvghmi
changeset 3139 1f5ca646ce6e
parent 3135 d723472a18a4
child 3142 2637bb6a6bb0
permissions -rw-r--r--
SVGHMI: Add inhibition to widget's apply_hmi_value() so that it does not change variable more frquently than given widget's frequency. This prevents flooding network with many update if browser is producing events at high rate, as for exemple when dragging ScrollBar's cursor.
// 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);

    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[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[index];
                    if(inhibition != undefined){
                        clearTimeout(inhibition);
                        this.lastapply[index] = undefined;
                        this.unhinibit(index);
                    }
                    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]";

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_svg_ns//*[@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";
        }
    }
}