SVGHMI: SVG viewport now defined so that HMI take scales and fit to the view. Implemented page switch through viewport change, no hiding of widget for now. svghmi
authorEdouard Tisserant
Mon, 28 Oct 2019 19:52:43 +0100
branchsvghmi
changeset 2808 dc78ffa5253d
parent 2807 7fa21b3b5f9f
child 2809 b9c540253263
SVGHMI: SVG viewport now defined so that HMI take scales and fit to the view. Implemented page switch through viewport change, no hiding of widget for now.
svghmi/gen_index_xhtml.xslt
svghmi/gen_index_xhtml.ysl2
svghmi/svghmi.js
--- a/svghmi/gen_index_xhtml.xslt	Mon Oct 28 10:30:20 2019 +0100
+++ b/svghmi/gen_index_xhtml.xslt	Mon Oct 28 19:52:43 2019 +0100
@@ -3,6 +3,7 @@
   <xsl:output method="xml" cdata-section-elements="xhtml:script"/>
   <xsl:variable name="geometry" select="ns:GetSVGGeometry()"/>
   <xsl:variable name="hmitree" select="ns:GetHMITree()"/>
+  <xsl:variable name="svg_root_id" select="/svg:svg/@id"/>
   <xsl:variable name="hmi_elements" select="//svg:*[starts-with(@inkscape:label, 'HMI:')]"/>
   <xsl:variable name="hmi_geometry" select="$geometry[@Id = $hmi_elements/@id]"/>
   <xsl:variable name="hmi_pages" select="$hmi_elements[func:parselabel(@inkscape:label)/widget/@type = 'Page']"/>
@@ -97,9 +98,25 @@
       </xsl:with-param>
     </xsl:apply-templates>
   </xsl:template>
-  <xsl:template mode="identity_svg" match="@* | node()">
+  <xsl:template mode="inline_svg" match="@* | node()">
     <xsl:copy>
-      <xsl:apply-templates mode="identity_svg" select="@* | node()"/>
+      <xsl:apply-templates mode="inline_svg" select="@* | node()"/>
+    </xsl:copy>
+  </xsl:template>
+  <xsl:template mode="inline_svg" match="svg:svg/@width"/>
+  <xsl:template mode="inline_svg" match="svg:svg/@height"/>
+  <xsl:template mode="inline_svg" match="svg:svg">
+    <xsl:copy>
+      <xsl:attribute name="preserveAspectRatio">
+        <xsl:text>none</xsl:text>
+      </xsl:attribute>
+      <xsl:attribute name="height">
+        <xsl:text>100vh</xsl:text>
+      </xsl:attribute>
+      <xsl:attribute name="width">
+        <xsl:text>100vw</xsl:text>
+      </xsl:attribute>
+      <xsl:apply-templates mode="inline_svg" select="@* | node()"/>
     </xsl:copy>
   </xsl:template>
   <xsl:template match="/">
@@ -108,19 +125,8 @@
     </xsl:comment>
     <html xmlns="http://www.w3.org/1999/xhtml">
       <head/>
-      <body style="margin:0;">
-        <xsl:copy>
-          <xsl:comment>
-            <xsl:apply-templates mode="testgeo" select="$hmi_geometry"/>
-          </xsl:comment>
-          <xsl:comment>
-            <xsl:apply-templates mode="testtree" select="$hmitree"/>
-          </xsl:comment>
-          <xsl:comment>
-            <xsl:apply-templates mode="testtree" select="$indexed_hmitree"/>
-          </xsl:comment>
-          <xsl:apply-templates mode="identity_svg" select="@* | node()"/>
-        </xsl:copy>
+      <body style="margin:0;overflow:hidden;">
+        <xsl:apply-templates mode="inline_svg" select="svg:svg"/>
         <script>
           <xsl:call-template name="scripts"/>
         </script>
@@ -288,6 +294,16 @@
       <xsl:value-of select="@id"/>
       <xsl:text>",
 </xsl:text>
+      <xsl:text>        bbox: [</xsl:text>
+      <xsl:value-of select="$p/@x"/>
+      <xsl:text>, </xsl:text>
+      <xsl:value-of select="$p/@y"/>
+      <xsl:text>, </xsl:text>
+      <xsl:value-of select="$p/@w"/>
+      <xsl:text>, </xsl:text>
+      <xsl:value-of select="$p/@h"/>
+      <xsl:text>],
+</xsl:text>
       <xsl:text>        widgets: [
 </xsl:text>
       <xsl:for-each select="$page_ids">
@@ -316,6 +332,10 @@
     <xsl:value-of select="$default_page"/>
     <xsl:text>";
 </xsl:text>
+    <xsl:text>var svg_root = document.getElementById("</xsl:text>
+    <xsl:value-of select="$svg_root_id"/>
+    <xsl:text>");
+</xsl:text>
     <xsl:text>// svghmi.js
 </xsl:text>
     <xsl:text>
@@ -696,20 +716,26 @@
 </xsl:text>
     <xsl:text>    }
 </xsl:text>
-    <xsl:text>    /* add new subsribers if any */
-</xsl:text>
-    <xsl:text>    if(new_desc) for(let widget of new_desc.widgets){
-</xsl:text>
-    <xsl:text>        for(let index of widget.indexes){
-</xsl:text>
-    <xsl:text>            subscribers[index].add(widget);
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    if(new_desc) {
+</xsl:text>
+    <xsl:text>        /* add new subsribers if any */
+</xsl:text>
+    <xsl:text>        for(let widget of new_desc.widgets){
+</xsl:text>
+    <xsl:text>            for(let index of widget.indexes){
+</xsl:text>
+    <xsl:text>                subscribers[index].add(widget);
+</xsl:text>
+    <xsl:text>            }
 </xsl:text>
     <xsl:text>        }
 </xsl:text>
+    <xsl:text>        svg_root.setAttribute('viewBox',new_desc.bbox.join(" "));
+</xsl:text>
     <xsl:text>    }
 </xsl:text>
-    <xsl:text>
-</xsl:text>
     <xsl:text>    current_page = page_name;
 </xsl:text>
     <xsl:text>
@@ -829,34 +855,14 @@
       </xsl:with-param>
     </xsl:apply-templates>
   </xsl:template>
-  <xsl:template mode="widget_defs" match="widget[@type='Display']">
+  <xsl:template name="defs_by_labels">
+    <xsl:param name="labels" select="''"/>
+    <xsl:param name="mandatory" select="'yes'"/>
     <xsl:param name="hmi_element"/>
-    <xsl:text>frequency: 5,
-</xsl:text>
-    <xsl:text>dispatch: function(value) {
-</xsl:text>
-    <xsl:choose>
-      <xsl:when test="$hmi_element[self::svg:text]">
-        <xsl:text>  this.element.textContent = String(value);
-</xsl:text>
-      </xsl:when>
-      <xsl:otherwise>
-        <xsl:message terminate="yes">
-          <xsl:text>Display widget as a group not implemented</xsl:text>
-        </xsl:message>
-      </xsl:otherwise>
-    </xsl:choose>
-    <xsl:text>},
-</xsl:text>
-  </xsl:template>
-  <xsl:template mode="widget_defs" match="widget[@type='Meter']">
-    <xsl:param name="hmi_element"/>
-    <xsl:text>frequency: 10,
-</xsl:text>
-    <xsl:for-each select="str:split('value min max needle range')">
+    <xsl:for-each select="str:split($labels)">
       <xsl:variable name="name" select="."/>
       <xsl:variable name="elt_id" select="$hmi_element//*[@inkscape:label=$name][1]/@id"/>
-      <xsl:if test="not($elt_id)">
+      <xsl:if test="$mandatory='yes' and not($elt_id)">
         <xsl:message terminate="yes">
           <xsl:text>Meter widget must have a </xsl:text>
           <xsl:value-of select="$name"/>
@@ -869,6 +875,37 @@
       <xsl:text>"),
 </xsl:text>
     </xsl:for-each>
+  </xsl:template>
+  <xsl:template mode="widget_defs" match="widget[@type='Display']">
+    <xsl:param name="hmi_element"/>
+    <xsl:text>frequency: 5,
+</xsl:text>
+    <xsl:text>dispatch: function(value) {
+</xsl:text>
+    <xsl:choose>
+      <xsl:when test="$hmi_element[self::svg:text]">
+        <xsl:text>  this.element.textContent = String(value);
+</xsl:text>
+      </xsl:when>
+      <xsl:otherwise>
+        <xsl:message terminate="yes">
+          <xsl:text>Display widget as a group not implemented</xsl:text>
+        </xsl:message>
+      </xsl:otherwise>
+    </xsl:choose>
+    <xsl:text>},
+</xsl:text>
+  </xsl:template>
+  <xsl:template mode="widget_defs" match="widget[@type='Meter']">
+    <xsl:param name="hmi_element"/>
+    <xsl:text>frequency: 10,
+</xsl:text>
+    <xsl:call-template name="defs_by_labels">
+      <xsl:with-param name="hmi_element" select="$hmi_element"/>
+      <xsl:with-param name="labels">
+        <test>value min max needle range</test>
+      </xsl:with-param>
+    </xsl:call-template>
     <xsl:text>dispatch: function(value) {
 </xsl:text>
     <xsl:text>    this.value_elt.textContent = String(value);
@@ -900,16 +937,12 @@
     <xsl:param name="hmi_element"/>
     <xsl:text>frequency: 5,
 </xsl:text>
-    <xsl:variable name="value_elt_id" select="$hmi_element//*[self::svg:text][@inkscape:label='value'][1]/@id"/>
-    <xsl:if test="not($value_elt_id)">
-      <xsl:message terminate="yes">
-        <xsl:text>Input widget must have a text element</xsl:text>
-      </xsl:message>
-    </xsl:if>
-    <xsl:text>value_elt: document.getElementById("</xsl:text>
-    <xsl:value-of select="$value_elt_id"/>
-    <xsl:text>"),
-</xsl:text>
+    <xsl:call-template name="defs_by_labels">
+      <xsl:with-param name="hmi_element" select="$hmi_element"/>
+      <xsl:with-param name="labels">
+        <test>value</test>
+      </xsl:with-param>
+    </xsl:call-template>
     <xsl:text>dispatch: function(value) {
 </xsl:text>
     <xsl:text>    this.value_elt.textContent = String(value);
@@ -955,4 +988,16 @@
     <xsl:text>    frequency: 5,
 </xsl:text>
   </xsl:template>
+  <xsl:template mode="widget_defs" match="widget[@type='Jump']">
+    <xsl:text>init: function() {
+</xsl:text>
+    <xsl:text>    this.element.addEventListener(
+</xsl:text>
+    <xsl:text>        "click", 
+</xsl:text>
+    <xsl:text>        evt =&gt; switch_page(this.args[0]));
+</xsl:text>
+    <xsl:text>},
+</xsl:text>
+  </xsl:template>
 </xsl:stylesheet>
--- a/svghmi/gen_index_xhtml.ysl2	Mon Oct 28 10:30:20 2019 +0100
+++ b/svghmi/gen_index_xhtml.ysl2	Mon Oct 28 19:52:43 2019 +0100
@@ -3,6 +3,11 @@
 // 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"{test *ptr};
+};
+
 istylesheet
             /* From Inkscape */
             xmlns:dc="http://purl.org/dc/elements/1.1/"
@@ -25,6 +30,7 @@
     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]";
 
@@ -96,9 +102,19 @@
      *  - copy every attributes 
      *  - copy every sub-elements
      */
-    template "@* | node()", mode="identity_svg" {
+    template "@* | node()", mode="inline_svg" {
       /* use real xsl:copy instead copy-of alias from yslt.yml2 */
-      xsl:copy apply "@* | node()", mode="identity_svg";
+      xsl:copy apply "@* | node()", mode="inline_svg";
+    }
+
+    /* replaces inkscape's height and width hints. forces fit */
+    template "svg:svg/@width", mode="inline_svg";
+    template "svg:svg/@height", mode="inline_svg";
+    template "svg:svg", mode="inline_svg" xsl:copy {
+        attrib "preserveAspectRatio" > none
+        attrib "height" > 100vh
+        attrib "width" > 100vw
+        apply "@* | node()", mode="inline_svg";
     }
 
     /*const "mark" > =HMI=\n*/
@@ -106,21 +122,21 @@
     /* 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";
+        }
+        */
         html xmlns="http://www.w3.org/1999/xhtml" {
             head;
-            body style="margin:0;" {
-                xsl:copy {
-                    comment {
-                        apply "$hmi_geometry", mode="testgeo";
-                    }
-                    comment {
-                        apply "$hmitree", mode="testtree";
-                    }
-                    comment {
-                        apply "$indexed_hmitree", mode="testtree";
-                    }
-                    apply "@* | node()", mode="identity_svg";
-                }
+            body style="margin:0;overflow:hidden;" {
+                apply "svg:svg", mode="inline_svg";
                 script{
                     call "scripts";
                 }
@@ -230,6 +246,7 @@
             const "page_elements", "$hmi_elements[@id = $page_ids]";
             |     "«$desc/arg[1]/@value»": {
             |         id: "«@id»",
+            |         bbox: [«$p/@x», «$p/@y», «$p/@w», «$p/@h»],
             |         widgets: [
             foreach "$page_ids" {
             |             hmi_widgets.«.»`if "position()!=last()" > ,`
@@ -241,7 +258,7 @@
 
         |
         | var default_page = "«$default_page»";
-
+        | var svg_root = document.getElementById("«$svg_root_id»");
         include text svghmi.js
         | //})();
     }
@@ -307,6 +324,20 @@
         };
     }
 
+
+    function "defs_by_labels" {
+        param "labels","''";
+        param "mandatory","'yes'";
+        param "hmi_element";
+        foreach "str:split($labels)" {
+            const "name",".";
+            const "elt_id","$hmi_element//*[@inkscape:label=$name][1]/@id";
+            if "$mandatory='yes' and not($elt_id)" error > Meter widget must have a «$name» element
+            | «$name»_elt: document.getElementById("«$elt_id»"),
+        }
+    }
+
+
     template "widget[@type='Display']", mode="widget_defs" {
         param "hmi_element";
         | frequency: 5,
@@ -326,12 +357,7 @@
     template "widget[@type='Meter']", mode="widget_defs" {
         param "hmi_element";
         | frequency: 10,
-        foreach "str:split('value min max needle range')" {
-            const "name",".";
-            const "elt_id","$hmi_element//*[@inkscape:label=$name][1]/@id";
-            if "not($elt_id)" error > Meter widget must have a «$name» element
-            | «$name»_elt: document.getElementById("«$elt_id»"),
-        }
+        labels("value min max needle range");
         | dispatch: function(value) {
         |     this.value_elt.textContent = String(value);
         |     let [min,max,totallength] = this.range;
@@ -350,9 +376,7 @@
     template "widget[@type='Input']", mode="widget_defs" {
         param "hmi_element";
         | frequency: 5,
-        const "value_elt_id","$hmi_element//*[self::svg:text][@inkscape:label='value'][1]/@id";
-        if "not($value_elt_id)" error > Input widget must have a text element
-        | value_elt: document.getElementById("«$value_elt_id»"),
+        labels("value");
         | dispatch: function(value) {
         |     this.value_elt.textContent = String(value);
         | },
@@ -378,16 +402,13 @@
         |     frequency: 5,
     }
     template "widget[@type='Change']", mode="widget_defs" {
-        // HMI:Change:-10@/PUMP/VALUE 
-        // HMI:Change:+1@/PUMP/VALUE 
-        // HMI:Change:=42@/PUMP/VALUE 
         |     frequency: 5,
     }
-    //    |     frequency: 10`apply ".", mode="refresh_frequency"`,
-    // template "widget", mode="refresh_frequency" > 10
-    /*
-    template "widget[@type='Meter']", mode="refresh_frequency" > 10
-    template "widget[@type='Display']", mode="refresh_frequency" > 5
-    template "widget[@type='Input']", mode="refresh_frequency" > 5
-    */
+    template "widget[@type='Jump']", mode="widget_defs" {
+        | init: function() {
+        |     this.element.addEventListener(
+        |         "click", 
+        |         evt => switch_page(this.args[0]));
+        | },
+    }
 }
--- a/svghmi/svghmi.js	Mon Oct 28 10:30:20 2019 +0100
+++ b/svghmi/svghmi.js	Mon Oct 28 19:52:43 2019 +0100
@@ -188,13 +188,16 @@
             subscribers[index].delete(widget);
         }
     }
-    /* add new subsribers if any */
-    if(new_desc) for(let widget of new_desc.widgets){
-        for(let index of widget.indexes){
-            subscribers[index].add(widget);
-        }
-    }
-
+
+    if(new_desc) {
+        /* add new subsribers if any */
+        for(let widget of new_desc.widgets){
+            for(let index of widget.indexes){
+                subscribers[index].add(widget);
+            }
+        }
+        svg_root.setAttribute('viewBox',new_desc.bbox.join(" "));
+    }
     current_page = page_name;
 
     update_subscriptions();