svghmi/gen_index_xhtml.ysl2
author Edouard Tisserant
Fri, 28 Feb 2020 16:09:21 +0100
branchsvghmi
changeset 2844 eee5dcd9fc92
parent 2843 94696b3f69fb
child 2845 61548f7d1bef
permissions -rw-r--r--
SVGHMI: detachable and discardable elements sets, Reworked geometric intersection, toward more accurate page content detection.

Moved page's widget/element dependency crawling functions so that it is possible to compute a global detachable and discardable elements sets.
Reworked geometric intersection detection logic to distinguish ovelapping and inclusion.
Goal is to include englobing and overlapping graphical elements, but not groups (would then include everything around...). Intermediate commit, to be continued.
include yslt_noindent.yml2

// overrides yslt's output function to set CDATA
decl output(method, cdata-section-elements="xhtml:script");

in xsl decl labels(*ptr, name="defs_by_labels") alias call-template {
    with "hmi_element", "$hmi_element";
    with "labels"{text *ptr};
};

in xsl decl optional_labels(*ptr, name="defs_by_labels") alias call-template {
    with "hmi_element", "$hmi_element";
    with "labels"{text *ptr};
    with "mandatory","'no'";
};

in xsl decl svgtmpl(match, xmlns="http://www.w3.org/2000/svg") alias template;

istylesheet
            /* From Inkscape */
            xmlns:dc="http://purl.org/dc/elements/1.1/"
            xmlns:cc="http://creativecommons.org/ns#"
            xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
            xmlns:svg="http://www.w3.org/2000/svg"
            xmlns:xlink="http://www.w3.org/1999/xlink"
            xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
            xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
            xmlns:xhtml="http://www.w3.org/1999/xhtml"

            /* Our namespace to invoke python code */
            xmlns:ns="beremiz"
            extension-element-prefixes="ns func"
            exclude-result-prefixes="ns str regexp exsl func" {

    /* This retrieves geometry obtained through "inkscape -S"
     * already parsed by python and presented as a list of
     * <bbox x="0" y="0" w="42" h="42">
     */
    const "geometry", "ns:GetSVGGeometry()";
    const "hmitree", "ns:GetHMITree()";

    const "svg_root_id", "/svg:svg/@id";
    const "hmi_elements", "//svg:*[starts-with(@inkscape:label, 'HMI:')]";
    const "hmi_geometry", "$geometry[@Id = $hmi_elements/@id]";

    const "hmi_pages", "$hmi_elements[func:parselabel(@inkscape:label)/widget/@type = 'Page']";

    const "default_page" choose {
        when "count($hmi_pages) > 1" {
            const "Home_page", 
                "$hmi_pages[func:parselabel(@inkscape:label)/widget/arg[1]/@value = 'Home']";
            choose {
                when "$Home_page" > Home
                otherwise {
                    error "No Home page defined!";
                }
            }
        }
        when "count($hmi_pages) = 0" {
            error "No page defined!";
        }
        otherwise > «func:parselabel($hmi_pages/@inkscape:label)/widget/arg[1]/@value»
    }

    const "_categories" {
        noindex > HMI_ROOT
        noindex > HMI_NODE
        noindex > HMI_PLC_STATUS
        noindex > HMI_CURRENT_PAGE
    }
    const "categories", "exsl:node-set($_categories)";
    const "_indexed_hmitree" apply "$hmitree", mode="index";
    const "indexed_hmitree", "exsl:node-set($_indexed_hmitree)";


    // returns all directly or indirectly refered elements
    def "func:refered_elements" {
        param "elems";
        const "descend", "$elems/descendant-or-self::svg:*";
        const "clones", "$descend[self::svg:use]";
        const "originals", "//svg:*[concat('#',@id) = $clones/@xlink:href]";
        choose {
            when "$originals" 
                result "$descend | func:refered_elements($originals)";
            otherwise
                result "$descend";
        }
    }

    def "func:intersect_1d" {
        /* it is assumed that a1 > a0 and b1 > b0 */
        param "a0";
        param "a1";
        param "b0";
        param "b1";

        const "d0", "$a0 >= $b0";
        const "d1", "$a1 >= $b1";
        choose {
            when "not($d0) and $d1"
                result "3"; /* a included in b */
            when "$d0 and not($d1)"
                result "2"; /* b included in a */
            when "$d0 = $d1 and $b0 < $a1"
                result "1"; /* a and b are overlapped */
            otherwise
                result "0"; /* no intersection*/
        }
    }

    def "func:intersect" {
        param "a";
        param "b";

        const "x_intersect", "func:intersect_1d($a/@x, $a/@x+$a/@w, $b/@x, $b/@x+$b/@w)";

        choose{
            when "$x_intersect != 0"{
                const "y_intersect", "func:intersect_1d($a/@y, $a/@y+$a/@w, $b/@y, $b/@y+$b/@w)";
                result "$x_intersect * $y_intersect";
            }
            otherwise result "0";
        }
    }

    // return overlapping geometry a given element
    def "func:overlapping_geometry" {
        param "elt";
        /* only included groups are returned */
        /* all other elemenst are returne when overlapping*/
        const "g", "$geometry[@Id = $elt/@id]"; 
        result """$geometry[@Id != $elt/@id and func:intersect(., $g) = 4]""";
    }

    def "func:sumarized_elements" {
        param "elements";
        const "short_list", "$elements[not(ancestor::*/@id = $elements/@id)]";
        /* TODO exclude globally discardable elements from group fulfillment check */
        const "filled_groups", "$short_list/parent::svg:*[not(descendant::*[not(self::svg:g)][not(@id = $short_list/descendant-or-self::*[not(self::svg:g)]/@id)])]";
        const "groups_to_add", "$filled_groups[not(ancestor::*/@id = $filled_groups/@id)]";
        result "$groups_to_add | $short_list[not(ancestor::svg:g/@id = $filled_groups/@id)]";
    }

    def "func:all_related_elements" {
        param "page";
        const "page_overlapping_geometry", "func:overlapping_geometry($page)";
        const "page_overlapping_elements", "//svg:*[@id = $page_overlapping_geometry/@Id]";
        const "page_sub_elements", "func:refered_elements($page | $page_overlapping_elements)";
        result "$page_sub_elements";
    }

    def "func:detachable_elements" {
        param "pages"; 
        choose{
            when "$pages"{
                result """func:sumarized_elements(func:all_related_elements($pages[1]))
                          | func:detachable_elements($pages[position()!=1])""";
            }otherwise{
                result "/..";
            }
        }
    }
    
    const "detachable_elements", "func:detachable_elements($hmi_pages)";
    const "essential_elements", "$detachable_elements | /svg:svg/svg:defs";
    const "required_elements", "$essential_elements//svg:* | $essential_elements/ancestor-or-self::svg:*";
    const "discardable_elements", "//svg:*[not(@id = $required_elements/@id)]";

    template "*", mode="index" {
        param "index", "0";
        param "parentpath", "''";
        const "content" {
            const "path"
                choose {
                    when "local-name() = 'HMI_ROOT'" > «$parentpath»
                    otherwise > «$parentpath»/«@name»
                }
            choose {
                when "not(local-name() = $categories/noindex)" {
                    xsl:copy {
                        attrib "index" > «$index»
                        attrib "hmipath" > «$path»
                        foreach "@*" xsl:copy;
                    }
                    /* no node expected below value nodes */
                }
                otherwise {
                    apply "*[1]", mode="index"{
                        with "index", "$index";
                        with "parentpath" > «$path»
                    }
                }
            }
        }

        copy "$content";
        apply "following-sibling::*[1]", mode="index" {
            with "index", "$index + count(exsl:node-set($content)/*)";
            with "parentpath" > «$parentpath»
        }
    }

    /* Identity template :
     *  - copy every attributes 
     *  - copy every sub-elements
     */
    template "@* | node()", mode="inline_svg" {
      /* use real xsl:copy instead copy-of alias from yslt.yml2 */
      xsl:copy apply "@* | node()", mode="inline_svg";
      /* TODO filter out globally discardable elements */
    }

    /* replaces inkscape's height and width hints. forces fit */
    template "svg:svg/@width", mode="inline_svg";
    template "svg:svg/@height", mode="inline_svg";
    svgtmpl "svg:svg", mode="inline_svg" svg {
        attrib "preserveAspectRatio" > none
        attrib "height" > 100vh
        attrib "width" > 100vw
        apply "@* | node()", mode="inline_svg";
    }
    /* ensure that coordinate in CSV file generated by inkscape are in default reference frame */
    template "svg:svg[@viewBox!=concat('0 0 ', @width, ' ', @height)]", mode="inline_svg" {
        error > ViewBox settings other than X=0, Y=0 and Scale=1 are not supported
    }
    /* ensure that coordinate in CSV file generated by inkscape match svg default unit */
    template "sodipodi:namedview[@units!='px' or @inkscape:document-units!='px']", mode="inline_svg" {
        error > All units must be set to "px" in Inkscape's document properties
    }

    
    //// Commented out before implementing runtime DOM remove/append on page switch - would have side effect
    ////
    //// /* clone unlinkink until widget for better perf with webkit */
    //// svgtmpl "svg:use", mode="inline_svg" 
    //// {
    ////     g{
    ////         attrib "style" > «@style»
    ////         attrib "transform" > «@transform»
    ////         /* keep same id and label in case it is a widget */
    ////         //attrib "inkscape:label","@inkscape:label";
    ////         attrib "id" > «@id»
    ////         const "targetid","substring-after(@xlink:href,'#')";
    ////         apply "//svg:*[@id = $targetid]", mode="unlink_clone";
    ////     }
    //// }
    //// svgtmpl "@*", mode="unlink_clone" xsl:copy;
    //// svgtmpl "svg:*", mode="unlink_clone" {
    ////     choose {
    ////         when "@id = $hmi_elements/@id" {
    ////             use{
    ////                 attrib "xlink:href" > «concat('#',@id)»
    ////             }
    ////         }
    ////         otherwise {
    ////             xsl:copy apply "@* | node()", mode="unlink_clone";
    ////         }
    ////     }
    //// }

    // template "svg:use/@style", mode="inline_svg"{
    //     attrib "style" > all:initial;
    //     //«.»
    // }

    // template "svg:*[concat('#',@id) = //svg:use/@xlink:href]/@style", mode="inline_svg"{
    //     attrib "style" > all:unset;
    //     //«.»
    // }

    /*const "mark" > =HMI=\n*/

    /* copy root node and add geometry as comment for a test */
    template "/" { 
        comment > Made with SVGHMI. https://beremiz.org
        /* DEBUG DATA */
        comment {
            apply "$hmi_geometry", mode="testgeo";
        }
        comment {
            apply "$hmitree", mode="testtree";
        }
        comment {
            apply "$indexed_hmitree", mode="testtree";
        }
        comment {
            | Detachable :
            foreach "$detachable_elements"{
                | «@id»
            }
        }
        comment {
            | Discardable :
            foreach "$discardable_elements"{
                | «@id»
            }
        }
        /**/
        html xmlns="http://www.w3.org/1999/xhtml"
             xmlns:svg="http://www.w3.org/2000/svg"
             xmlns:xlink="http://www.w3.org/1999/xlink" {
            head;
            body style="margin:0;overflow:hidden;" {
                apply "svg:svg", mode="inline_svg";
                script{
                    call "scripts";
                }
            }
        }
    }

    /*
        Parses:
        "HMI:WidgetType:param1:param2@path1@path2"

        Into:
        widget type="WidgetType" {
            arg value="param1";
            arg value="param2";
            path value="path1";
            path value="path2";
        }
    */

    def "func:parselabel" {
        param "label";
        const "description", "substring-after($label,'HMI:')";

        const "_args", "substring-before($description,'@')";
        const "args" choose {
            when "$_args" value "$_args";
            otherwise value "$description";
        }

        const "_type", "substring-before($args,':')";
        const "type" choose {
            when "$_type" value "$_type";
            otherwise value "$args";
        }

        const "ast" if "$type" widget {
            attrib "type" > «$type»
            foreach "str:split(substring-after($args, ':'), ':')" {
                arg {
                    attrib "value" > «.»
                }
            }
            const "paths", "substring-after($description,'@')";
            foreach "str:split($paths, '@')" {
                path {
                    attrib "value" > «.»
                }
            }
        }

        result "exsl:node-set($ast)";
    }

    function "scripts"
    {
        | //(function(){
        |
        | var hmi_hash = [«$hmitree/@hash»]; 

        /* TODO re-enable
        ||
        function evaluate_js_from_descriptions() {
            var Page;
            var Input;
            var Display;
            var res = [];
        ||
        const "midmark" > \n«$mark»
        apply """//*[contains(child::svg:desc, $midmark) or \
                     starts-with(child::svg:desc, $mark)]""",2 
              mode="code_from_descs";
        ||
            return res;
        }
        ||
        */

        | var hmi_widgets = {
        foreach "$hmi_elements" {
            const "widget", "func:parselabel(@inkscape:label)/widget";
            | "«@id»": {
            |     type: "«$widget/@type»",
            |     args: [
            foreach "$widget/arg"
            |         "«@value»"`if "position()!=last()" > ,`
            |     ],
            |     indexes: [
            foreach "$widget/path" {
                const "hmipath","@value";
                const "hmitree_match","$indexed_hmitree/*[@hmipath = $hmipath]";
                choose {
                    when "count($hmitree_match) = 0" {
                        warning > No match for path "«$hmipath»" in HMI tree
                    }
                    otherwise {
            |         «$hmitree_match/@index»`if "position()!=last()" > ,`
                    }
                }
            }
            |     ],
            |     element: document.getElementById("«@id»"),
            apply "$widget", mode="widget_defs" with "hmi_element",".";
            | }`if "position()!=last()" > ,`
        }
        | }
        |
        | var heartbeat_index = «$indexed_hmitree/*[@hmipath = '/HEARTBEAT']/@index»;
        |
        | var hmitree_types = [

        foreach "$indexed_hmitree/*" {
            | /* «@index»  «@hmipath» */ "«substring(local-name(), 5)»"`if "position()!=last()" > ,`
        }

        | ]
        |

        | var page_desc = {

        foreach "$hmi_pages" {
            const "desc", "func:parselabel(@inkscape:label)/widget";
            const "page", ".";
            const "p", "$geometry[@Id = $page/@id]"; 

            const "page_all_elements", "func:all_related_elements($page)";

            const "all_page_ids","$page_all_elements[@id = $hmi_elements/@id and @id != $page/@id]/@id";

            const "shorter_list", "func:sumarized_elements($page_all_elements)";

            |     "«$desc/arg[1]/@value»": {
            |         widget: hmi_widgets["«@id»"],
            |         bbox: [«$p/@x», «$p/@y», «$p/@w», «$p/@h»],
            |         widgets: [
            foreach "$all_page_ids" {
            |             hmi_widgets["«.»"]`if "position()!=last()" > ,`
            }
            |         ],
            |         required_elements: [
            foreach "$shorter_list" {
            |             "«@id»",
            }
            |         ]
            |     }`if "position()!=last()" > ,`
        }
        | }

        |
        | var default_page = "«$default_page»";
        | var svg_root = document.getElementById("«$svg_root_id»");
        include text svghmi.js
        | //})();
    }

    // template "*", mode="code_from_descs" {
    //     ||
    //     {
    //         var path, role, name, priv;
    //         var id = "«@id»";
    //     ||

    //     /* if label is used, use it as default name */
    //     if "@inkscape:label"
    //         |> name = "«@inkscape:label»";

    //     | /* -------------- */

    //     // this breaks indent, but fixing indent could break string literals
    //     value "substring-after(svg:desc, $mark)";
    //     // nobody reads generated code anyhow...

    //     ||

    //         /* -------------- */
    //         res.push({
    //             path:path,
    //             role:role,
    //             name:name,
    //             priv:priv
    //         })
    //     }
    //     ||
    // }


    /**/
    template "bbox", mode="testgeo"{
        | ID: «@Id» x: «@x» y: «@y» w: «@w» h: «@h»
    }

    template "*", mode="testtree"{
        param "indent", "''";
        > «$indent» «local-name()» 
        foreach "@*" > «local-name()»=«.» 
        > \n
        apply "*", mode="testtree" {
            with "indent" value "concat($indent,'>')"
        };
    }
    /**/

    function "defs_by_labels" {
        param "labels","''";
        param "mandatory","'yes'";
        param "hmi_element";
        const "widget_type","@type";
        foreach "str:split($labels)" {
            const "name",".";
            const "elt_id","$hmi_element//*[@inkscape:label=$name][1]/@id";
            choose {
                when "not($elt_id)" {
                    if "$mandatory='yes'" {
                        // TODO FIXME error > «$widget_type» widget must have a «$name» element
                        warning > «$widget_type» widget must have a «$name» element
                    }
                    // otherwise produce nothing 
                }
                otherwise {
                    | «$name»_elt: document.getElementById("«$elt_id»"),
                }
            }
        }
    }


    template "widget[@type='Display']", mode="widget_defs" {
        param "hmi_element";
        | frequency: 5,
        | dispatch: function(value) {
        choose {
            when "$hmi_element[self::svg:text]"{
            // TODO : care about <tspan> ?
        |   this.element.textContent = String(value);
            }
            otherwise {
                warning > Display widget as a group not implemented
            }
        }
        | },

    }
    template "widget[@type='Meter']", mode="widget_defs" {
        param "hmi_element";
        | frequency: 10,
        labels("value min max needle range");
        | dispatch: function(value) {
        |     this.value_elt.textContent = String(value);
        |     let [min,max,totallength] = this.range;
        |     let length = Math.max(0,Math.min(totallength,(Number(value)-min)*totallength/(max-min)));
        |     let tip = this.range_elt.getPointAtLength(length);
        // TODO : deal with transformations between needle and range
        |     this.needle_elt.setAttribute('d', "M "+this.origin.x+","+this.origin.y+" "+tip.x+","+tip.y);
        | },
        | origin: undefined,
        | range: undefined,
        | init: function() {
        |     this.range = [Number(this.min_elt.textContent), Number(this.max_elt.textContent), this.range_elt.getTotalLength()]
        |     this.origin = this.needle_elt.getPointAtLength(0);
        | },
    }

    def "func:escape_quotes" {
        param "txt";
        // have to use a python string to enter escaped quote
        const "frst", !"substring-before($txt,'\"')"!;
        const "frstln", "string-length($frst)";
        choose {
            when "$frstln > 0 and string-length($txt) > $frstln" {
                result !"concat($frst,'\\\"',func:escape_quotes(substring-after($txt,'\"')))"!;
            }
            otherwise {
                result "$txt";
            }
        }
    }

    template "widget[@type='Input']", mode="widget_defs" {
        param "hmi_element";
        const "value_elt" {
            optional_labels("value");
        }
        value "$value_elt";
        if "$value_elt"
            | frequency: 5,
        | dispatch: function(value) {
        if "$value_elt"
        |     this.value_elt.textContent = String(value);
        | },
        const "edit_elt_id","$hmi_element/*[@inkscape:label='edit'][1]/@id";
        | init: function() {
        if "$edit_elt_id" {
        |     document.getElementById("«$edit_elt_id»").addEventListener(
        |         "click", 
        |         evt => alert('XXX TODO : Edit value'));
        }
        foreach "$hmi_element/*[regexp:test(@inkscape:label,'^[=+\-].+')]" {
        |     document.getElementById("«@id»").addEventListener(
        |         "click", 
        |         evt => {let new_val = change_hmi_value(this.indexes[0], "«func:escape_quotes(@inkscape:label)»");
        |                 this.value_elt.textContent = String(new_val);});
                          /* could gray out value until refreshed */
        }
        | },
    }
    template "widget[@type='Button']", mode="widget_defs" {
    }
    template "widget[@type='Toggle']", mode="widget_defs" {
        |     frequency: 5,
    }
    template "widget[@type='Switch']", mode="widget_defs" {
        param "hmi_element";
        | frequency: 5,
        | dispatch: function(value) {
        |     for(let choice of this.choices){
        |         if(value != choice.value){
        |             choice.elt.setAttribute("style", "display:none");
        |         } else {
        |             choice.elt.setAttribute("style", choice.style);
        |         }
        |     }
        | },
        | init: function() {
        |     // Hello Switch
        | },
        | choices: [
        const "regex",!"'^(\"[^\"].*\"|\-?[0-9]+)(#.*)?$'"!;
        foreach "$hmi_element/*[regexp:test(@inkscape:label,$regex)]" {
            const "literal", "regexp:match(@inkscape:label,$regex)[2]";
        |     {
        |         elt:document.getElementById("«@id»"),
        |         style:"«@style»",
        |         value:«$literal»
        |     }`if "position()!=last()" > ,`
        }
        | ],
    }
    template "widget[@type='Jump']", mode="widget_defs" {
        param "hmi_element";
        | on_click: function(evt) {
        |     console.log(evt);
        |     switch_page(this.args[0]);
        | },
        | init: function() {
        /* registering event this way doies not "click" through svg:use 
        |     this.element.onclick = evt => switch_page(this.args[0]);
        event must be registered by adding attribute to element instead
        TODO : generalize mouse event handling by global event capture + getElementsAtPoint()
        */
        |     this.element.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_click(evt)");
        | },
    }
}