# HG changeset patch # User Edouard Tisserant # Date 1664953578 -7200 # Node ID f117526d41bae50c47bb782d17396c3111a9ede1 # Parent c1796e57affd2530bd11e5777edba13b324892b9 SVGHMI: update generated XSLT diff -r c1796e57affd -r f117526d41ba svghmi/analyse_widget.xslt --- a/svghmi/analyse_widget.xslt Tue Oct 04 11:06:04 2022 +0200 +++ b/svghmi/analyse_widget.xslt Wed Oct 05 09:06:18 2022 +0200 @@ -262,6 +262,42 @@ <xsl:text>speed</xsl:text> </path> </xsl:template> + <xsl:template match="widget[@type='Assign']" mode="widget_desc"> + <type> + <xsl:value-of select="@type"/> + </type> + <longdesc> + <xsl:text> +</xsl:text> + <xsl:text>Arguments are either: +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>- name=value: setting variable with literal value. +</xsl:text> + <xsl:text>- name=other_name: copy variable content into another +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>"active"+"inactive" labeled elements can be provided to show feedback when pressed +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>Exemples: +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>HMI:Assign:notify=1@notify=/PLCVAR +</xsl:text> + <xsl:text>HMI:Assign:ack=2:notify=1@ack=.local_var@notify=/PLCVAR +</xsl:text> + <xsl:text> +</xsl:text> + </longdesc> + <shortdesc> + <xsl:text>Assign variables on click</xsl:text> + </shortdesc> + </xsl:template> <xsl:template match="widget[@type='Back']" mode="widget_desc"> <type> <xsl:value-of select="@type"/> diff -r c1796e57affd -r f117526d41ba svghmi/gen_index_xhtml.xslt --- a/svghmi/gen_index_xhtml.xslt Tue Oct 04 11:06:04 2022 +0200 +++ b/svghmi/gen_index_xhtml.xslt Wed Oct 05 09:06:18 2022 +0200 @@ -555,6 +555,23 @@ <xsl:variable name="candidates" select="$geometry[@Id != $elt/@id]"/> <func:result select="$candidates[(@Id = $groups/@id and (func:intersect($g, .) = 9)) or (not(@Id = $groups/@id) and (func:intersect($g, .) > 0 ))]"/> </func:function> + <func:function name="func:offset"> + <xsl:param name="elt1"/> + <xsl:param name="elt2"/> + <xsl:variable name="g1" select="$geometry[@Id = $elt1/@id]"/> + <xsl:variable name="g2" select="$geometry[@Id = $elt2/@id]"/> + <xsl:variable name="result"> + <vector> + <xsl:attribute name="x"> + <xsl:value-of select="$g2/@x - $g1/@x"/> + </xsl:attribute> + <xsl:attribute name="y"> + <xsl:value-of select="$g2/@y - $g1/@y"/> + </xsl:attribute> + </vector> + </xsl:variable> + <func:result select="exsl:node-set($result)"/> + </func:function> <xsl:variable name="hmi_lists_descs" select="$parsed_widgets/widget[@type = 'List']"/> <xsl:variable name="hmi_lists" select="$hmi_elements[@id = $hmi_lists_descs/@id]"/> <xsl:variable name="hmi_textlists_descs" select="$parsed_widgets/widget[@type = 'TextList']"/> @@ -657,7 +674,8 @@ <xsl:param name="page"/> <xsl:variable name="page_overlapping_geometry" select="$overlapping_geometry/elt[@id = $page/@id]/*"/> <xsl:variable name="page_overlapping_elements" select="//svg:*[@id = $page_overlapping_geometry/@Id]"/> - <xsl:variable name="page_sub_elements" select="func:refered_elements($page | $page_overlapping_elements)"/> + <xsl:variable name="page_widgets_elements" select=" $hmi_elements[not(@id=$page/@id) and descendant-or-self::svg:*/@id = $page_overlapping_elements/@id] /descendant-or-self::svg:*"/> + <xsl:variable name="page_sub_elements" select="func:refered_elements($page | $page_overlapping_elements | $page_widgets_elements)"/> <func:result select="$page_sub_elements"/> </func:function> <func:function name="func:required_elements"> @@ -890,6 +908,14 @@ <xsl:text> </xsl:text> </xsl:for-each> + <xsl:text>DISCARDABLES: +</xsl:text> + <xsl:for-each select="$discardable_elements"> + <xsl:text> </xsl:text> + <xsl:value-of select="@id"/> + <xsl:text> +</xsl:text> + </xsl:for-each> <xsl:text>In Foreach: </xsl:text> <xsl:for-each select="$in_forEach_widget_ids"> @@ -945,6 +971,21 @@ <xsl:value-of select="substring(., 2)"/> </xsl:attribute> </xsl:template> + <xsl:template xmlns="http://www.w3.org/2000/svg" mode="inline_svg" match="svg:rect[@inkscape:label='reference' or @inkscape:label='frame']"/> + <xsl:template xmlns="http://www.w3.org/2000/svg" mode="inline_svg" match="svg:g[svg:rect/@inkscape:label='frame']"> + <xsl:variable name="reference_rect" select="(../svg:rect | ../svg:g/svg:rect)[@inkscape:label='reference']"/> + <xsl:variable name="frame_rect" select="svg:rect[@inkscape:label='frame']"/> + <xsl:variable name="offset" select="func:offset($frame_rect, $reference_rect)"/> + <xsl:copy> + <xsl:attribute name="svghmi_x_offset"> + <xsl:value-of select="$offset/vector/@x"/> + </xsl:attribute> + <xsl:attribute name="svghmi_y_offset"> + <xsl:value-of select="$offset/vector/@y"/> + </xsl:attribute> + <xsl:apply-templates mode="inline_svg" select="@* | node()"/> + </xsl:copy> + </xsl:template> <xsl:variable name="targets_not_to_unlink" select="$hmi_lists/descendant-or-self::svg:*"/> <xsl:variable name="to_unlink" select="$hmi_widgets/descendant-or-self::svg:use"/> <func:function name="func:is_unlinkable"> @@ -1516,8 +1557,6 @@ </xsl:text> <xsl:text>var cache = hmitree_types.map(_ignored => undefined); </xsl:text> - <xsl:text>var updates = new Map(); -</xsl:text> <xsl:text> </xsl:text> <xsl:text>function page_local_index(varname, pagename){ @@ -1530,7 +1569,7 @@ </xsl:text> <xsl:text> new_index = next_available_index++; </xsl:text> - <xsl:text> hmi_locals[pagename] = {[varname]:new_index} + <xsl:text> hmi_locals[pagename] = {[varname]:new_index}; </xsl:text> <xsl:text> } else { </xsl:text> @@ -1556,8 +1595,6 @@ </xsl:text> <xsl:text> cache[new_index] = defaultval; </xsl:text> - <xsl:text> updates.set(new_index, defaultval); -</xsl:text> <xsl:text> if(persistent_locals.has(varname)) </xsl:text> <xsl:text> persistent_indexes.set(new_index, varname); @@ -2656,6 +2693,199 @@ <xsl:text>} </xsl:text> </xsl:template> + <xsl:template match="widget[@type='Assign']" mode="widget_desc"> + <type> + <xsl:value-of select="@type"/> + </type> + <longdesc> + <xsl:text> +</xsl:text> + <xsl:text>Arguments are either: +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>- name=value: setting variable with literal value. +</xsl:text> + <xsl:text>- name=other_name: copy variable content into another +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>"active"+"inactive" labeled elements can be provided to show feedback when pressed +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>Exemples: +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>HMI:Assign:notify=1@notify=/PLCVAR +</xsl:text> + <xsl:text>HMI:Assign:ack=2:notify=1@ack=.local_var@notify=/PLCVAR +</xsl:text> + <xsl:text> +</xsl:text> + </longdesc> + <shortdesc> + <xsl:text>Assign variables on click</xsl:text> + </shortdesc> + </xsl:template> + <xsl:template match="widget[@type='Assign']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>AssignWidget</xsl:text> + <xsl:text> extends Widget{ +</xsl:text> + <xsl:text> frequency = 2; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> onmouseup(evt) { +</xsl:text> + <xsl:text> svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); +</xsl:text> + <xsl:text> if(this.enable_state) { +</xsl:text> + <xsl:text> this.activity_state = false +</xsl:text> + <xsl:text> this.request_animate(); +</xsl:text> + <xsl:text> this.assign(); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> onmousedown(){ +</xsl:text> + <xsl:text> if(this.enable_state) { +</xsl:text> + <xsl:text> svg_root.addEventListener("pointerup", this.bound_onmouseup, true); +</xsl:text> + <xsl:text> this.activity_state = true; +</xsl:text> + <xsl:text> this.request_animate(); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>} +</xsl:text> + </xsl:template> + <xsl:template match="widget[@type='Assign']" mode="widget_defs"> + <xsl:param name="hmi_element"/> + <xsl:variable name="disability"> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>/disabled</xsl:text> + </xsl:with-param> + <xsl:with-param name="mandatory" select="'no'"/> + </xsl:call-template> + </xsl:variable> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:text> activable_sub:{ +</xsl:text> + <xsl:variable name="activity"> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>/active /inactive</xsl:text> + </xsl:with-param> + <xsl:with-param name="mandatory"> + <xsl:text>no</xsl:text> + </xsl:with-param> + </xsl:call-template> + </xsl:variable> + <xsl:value-of select="$activity"/> + <xsl:variable name="has_activity" select="string-length($activity)>0"/> + <xsl:text> }, +</xsl:text> + <xsl:text> has_activity: </xsl:text> + <xsl:value-of select="$has_activity"/> + <xsl:text>, +</xsl:text> + <xsl:text> init: function() { +</xsl:text> + <xsl:text> this.bound_onmouseup = this.onmouseup.bind(this); +</xsl:text> + <xsl:text> this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); +</xsl:text> + <xsl:text> }, +</xsl:text> + <xsl:text> assignments: {}, +</xsl:text> + <xsl:text> dispatch: function(value, oldval, varnum) { +</xsl:text> + <xsl:variable name="widget" select="."/> + <xsl:for-each select="path"> + <xsl:variable name="varid" select="generate-id()"/> + <xsl:variable name="varnum" select="position()-1"/> + <xsl:if test="@assign"> + <xsl:for-each select="$widget/path[@assign]"> + <xsl:if test="$varid = generate-id()"> + <xsl:text> if(varnum == </xsl:text> + <xsl:value-of select="$varnum"/> + <xsl:text>) this.assignments["</xsl:text> + <xsl:value-of select="@assign"/> + <xsl:text>"] = value; +</xsl:text> + </xsl:if> + </xsl:for-each> + </xsl:if> + </xsl:for-each> + <xsl:text> }, +</xsl:text> + <xsl:text> assign: function() { +</xsl:text> + <xsl:variable name="paths" select="path"/> + <xsl:for-each select="arg[contains(@value,'=')]"> + <xsl:variable name="name" select="substring-before(@value,'=')"/> + <xsl:variable name="value" select="substring-after(@value,'=')"/> + <xsl:variable name="index"> + <xsl:for-each select="$paths"> + <xsl:if test="@assign = $name"> + <xsl:value-of select="position()-1"/> + </xsl:if> + </xsl:for-each> + </xsl:variable> + <xsl:variable name="isVarName" select="regexp:test($value,'^[a-zA-Z_][a-zA-Z0-9_]+$')"/> + <xsl:choose> + <xsl:when test="$isVarName"> + <xsl:text> const </xsl:text> + <xsl:value-of select="$value"/> + <xsl:text> = this.assignments["</xsl:text> + <xsl:value-of select="$value"/> + <xsl:text>"]; +</xsl:text> + <xsl:text> if(</xsl:text> + <xsl:value-of select="$value"/> + <xsl:text> != undefined) +</xsl:text> + <xsl:text> this.apply_hmi_value(</xsl:text> + <xsl:value-of select="$index"/> + <xsl:text>, </xsl:text> + <xsl:value-of select="$value"/> + <xsl:text>); +</xsl:text> + </xsl:when> + <xsl:otherwise> + <xsl:text> this.apply_hmi_value(</xsl:text> + <xsl:value-of select="$index"/> + <xsl:text>, </xsl:text> + <xsl:value-of select="$value"/> + <xsl:text>); +</xsl:text> + </xsl:otherwise> + </xsl:choose> + </xsl:for-each> + <xsl:text> }, +</xsl:text> + </xsl:template> <xsl:template match="widget[@type='Back']" mode="widget_desc"> <type> <xsl:value-of select="@type"/> @@ -5923,39 +6153,31 @@ </xsl:text> <xsl:text> frequency = 2; </xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> make_on_click() { -</xsl:text> - <xsl:text> let that = this; -</xsl:text> - <xsl:text> const name = this.args[0]; -</xsl:text> - <xsl:text> return function(evt){ -</xsl:text> - <xsl:text> /* TODO: in order to allow jumps to page selected through -</xsl:text> - <xsl:text> for exemple a dropdown, support path pointing to local -</xsl:text> - <xsl:text> variable whom value would be an HMI_TREE index and then -</xsl:text> - <xsl:text> jump to a relative page not hard-coded in advance -</xsl:text> - <xsl:text> */ -</xsl:text> - <xsl:text> if(that.enable_state) { -</xsl:text> - <xsl:text> const index = -</xsl:text> - <xsl:text> (that.is_relative && that.indexes.length > 0) ? -</xsl:text> - <xsl:text> that.indexes[0] + that.offset : undefined; -</xsl:text> - <xsl:text> fading_page_switch(name, index); -</xsl:text> - <xsl:text> that.notify(); -</xsl:text> - <xsl:text> } + <xsl:text> target_page_is_current_page = false; +</xsl:text> + <xsl:text> button_beeing_pressed = false; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> onmouseup(evt) { +</xsl:text> + <xsl:text> svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); +</xsl:text> + <xsl:text> if(this.enable_state) { +</xsl:text> + <xsl:text> const index = +</xsl:text> + <xsl:text> (this.is_relative && this.indexes.length > 0) ? +</xsl:text> + <xsl:text> this.indexes[0] + this.offset : undefined; +</xsl:text> + <xsl:text> this.button_beeing_pressed = false; +</xsl:text> + <xsl:text> this.activity_state = this.target_page_is_current_page || this.button_beeing_pressed; +</xsl:text> + <xsl:text> fading_page_switch(this.args[0], index); +</xsl:text> + <xsl:text> this.notify(); </xsl:text> <xsl:text> } </xsl:text> @@ -5963,6 +6185,24 @@ </xsl:text> <xsl:text> </xsl:text> + <xsl:text> onmousedown(){ +</xsl:text> + <xsl:text> if(this.enable_state) { +</xsl:text> + <xsl:text> svg_root.addEventListener("pointerup", this.bound_onmouseup, true); +</xsl:text> + <xsl:text> this.button_beeing_pressed = true; +</xsl:text> + <xsl:text> this.activity_state = true; +</xsl:text> + <xsl:text> this.request_animate(); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> <xsl:text> notify_page_change(page_name, index) { </xsl:text> <xsl:text> // called from animate() @@ -5973,7 +6213,9 @@ </xsl:text> <xsl:text> const ref_name = this.args[0]; </xsl:text> - <xsl:text> this.activity_state = ((ref_name == undefined || ref_name == page_name) && index == ref_index); + <xsl:text> this.target_page_is_current_page = ((ref_name == undefined || ref_name == page_name) && index == ref_index); +</xsl:text> + <xsl:text> this.activity_state = this.target_page_is_current_page || this.button_beeing_pressed; </xsl:text> <xsl:text> // Since called from animate, update activity directly </xsl:text> @@ -6031,7 +6273,9 @@ <xsl:variable name="jump_disability" select="$has_activity and $has_disability"/> <xsl:text> init: function() { </xsl:text> - <xsl:text> this.element.onclick = this.make_on_click(); + <xsl:text> this.bound_onmouseup = this.onmouseup.bind(this); +</xsl:text> + <xsl:text> this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); </xsl:text> <xsl:if test="$has_activity"> <xsl:text> this.activable = true; @@ -11091,1000 +11335,1064 @@ </xsl:text> <xsl:text> </xsl:text> - <xsl:text>// Apply updates recieved through ws.onmessage to subscribed widgets -</xsl:text> - <xsl:text>function apply_updates() { -</xsl:text> - <xsl:text> updates.forEach((value, index) => { + <xsl:text>// Called on requestAnimationFrame, modifies DOM +</xsl:text> + <xsl:text>var requestAnimationFrameID = null; +</xsl:text> + <xsl:text>function animate() { +</xsl:text> + <xsl:text> let rearm = true; +</xsl:text> + <xsl:text> do{ +</xsl:text> + <xsl:text> if(page_fading == "pending" || page_fading == "forced"){ +</xsl:text> + <xsl:text> if(page_fading == "pending") +</xsl:text> + <xsl:text> svg_root.classList.add("fade-out-page"); +</xsl:text> + <xsl:text> page_fading = "in_progress"; +</xsl:text> + <xsl:text> if(page_fading_args.length) +</xsl:text> + <xsl:text> setTimeout(function(){ +</xsl:text> + <xsl:text> switch_page(...page_fading_args); +</xsl:text> + <xsl:text> },1); +</xsl:text> + <xsl:text> break; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> // Do the page swith if pending +</xsl:text> + <xsl:text> if(page_switch_in_progress){ +</xsl:text> + <xsl:text> if(current_subscribed_page != current_visible_page){ +</xsl:text> + <xsl:text> switch_visible_page(current_subscribed_page); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> page_switch_in_progress = false; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(page_fading == "in_progress"){ +</xsl:text> + <xsl:text> svg_root.classList.remove("fade-out-page"); +</xsl:text> + <xsl:text> page_fading = "off"; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(jumps_need_update) update_jumps(); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> pending_widget_animates.forEach(widget => widget._animate()); +</xsl:text> + <xsl:text> pending_widget_animates = []; +</xsl:text> + <xsl:text> rearm = false; +</xsl:text> + <xsl:text> } while(0); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> requestAnimationFrameID = null; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(rearm) requestHMIAnimation(); +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function requestHMIAnimation() { +</xsl:text> + <xsl:text> if(requestAnimationFrameID == null){ +</xsl:text> + <xsl:text> requestAnimationFrameID = window.requestAnimationFrame(animate); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>// Message reception handler +</xsl:text> + <xsl:text>// Hash is verified and HMI values updates resulting from binary parsing +</xsl:text> + <xsl:text>// are stored until browser can compute next frame, DOM is left untouched +</xsl:text> + <xsl:text>ws.onmessage = function (evt) { +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> let data = evt.data; +</xsl:text> + <xsl:text> let dv = new DataView(data); +</xsl:text> + <xsl:text> let i = 0; +</xsl:text> + <xsl:text> try { +</xsl:text> + <xsl:text> for(let hash_int of hmi_hash) { +</xsl:text> + <xsl:text> if(hash_int != dv.getUint8(i)){ +</xsl:text> + <xsl:text> throw new Error("Hash doesn't match"); +</xsl:text> + <xsl:text> }; +</xsl:text> + <xsl:text> i++; +</xsl:text> + <xsl:text> }; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> while(i < data.byteLength){ +</xsl:text> + <xsl:text> let index = dv.getUint32(i, true); +</xsl:text> + <xsl:text> i += 4; +</xsl:text> + <xsl:text> let iectype = hmitree_types[index]; +</xsl:text> + <xsl:text> if(iectype != undefined){ +</xsl:text> + <xsl:text> let dvgetter = dvgetters[iectype]; +</xsl:text> + <xsl:text> let [value, bytesize] = dvgetter(dv,i); +</xsl:text> + <xsl:text> dispatch_value(index, value); +</xsl:text> + <xsl:text> i += bytesize; +</xsl:text> + <xsl:text> } else { +</xsl:text> + <xsl:text> throw new Error("Unknown index "+index); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> }; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> // register for rendering on next frame, since there are updates +</xsl:text> + <xsl:text> } catch(err) { +</xsl:text> + <xsl:text> // 1003 is for "Unsupported Data" +</xsl:text> + <xsl:text> // ws.close(1003, err.message); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> // TODO : remove debug alert ? +</xsl:text> + <xsl:text> alert("Error : "+err.message+"\nHMI will be reloaded."); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> // force reload ignoring cache +</xsl:text> + <xsl:text> location.reload(true); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text>}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>hmi_hash_u8 = new Uint8Array(hmi_hash); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function send_blob(data) { +</xsl:text> + <xsl:text> if(data.length > 0) { +</xsl:text> + <xsl:text> ws.send(new Blob([hmi_hash_u8].concat(data))); +</xsl:text> + <xsl:text> }; +</xsl:text> + <xsl:text>}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>const typedarray_types = { +</xsl:text> + <xsl:text> INT: (number) => new Int16Array([number]), +</xsl:text> + <xsl:text> BOOL: (truth) => new Int16Array([truth]), +</xsl:text> + <xsl:text> NODE: (truth) => new Int16Array([truth]), +</xsl:text> + <xsl:text> REAL: (number) => new Float32Array([number]), +</xsl:text> + <xsl:text> STRING: (str) => { +</xsl:text> + <xsl:text> // beremiz default string max size is 128 +</xsl:text> + <xsl:text> str = str.slice(0,128); +</xsl:text> + <xsl:text> binary = new Uint8Array(str.length + 1); +</xsl:text> + <xsl:text> binary[0] = str.length; +</xsl:text> + <xsl:text> for(let i = 0; i < str.length; i++){ +</xsl:text> + <xsl:text> binary[i+1] = str.charCodeAt(i); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> return binary; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> /* TODO */ +</xsl:text> + <xsl:text>}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function send_reset() { +</xsl:text> + <xsl:text> send_blob(new Uint8Array([1])); /* reset = 1 */ +</xsl:text> + <xsl:text>}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>var subscriptions = []; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function subscribers(index) { +</xsl:text> + <xsl:text> let entry = subscriptions[index]; +</xsl:text> + <xsl:text> let res; +</xsl:text> + <xsl:text> if(entry == undefined){ +</xsl:text> + <xsl:text> res = new Set(); +</xsl:text> + <xsl:text> subscriptions[index] = [res,0]; +</xsl:text> + <xsl:text> }else{ +</xsl:text> + <xsl:text> [res, _ign] = entry; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> return res +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function get_subscription_period(index) { +</xsl:text> + <xsl:text> let entry = subscriptions[index]; +</xsl:text> + <xsl:text> if(entry == undefined) +</xsl:text> + <xsl:text> return 0; +</xsl:text> + <xsl:text> let [_ign, period] = entry; +</xsl:text> + <xsl:text> return period; +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function set_subscription_period(index, period) { +</xsl:text> + <xsl:text> let entry = subscriptions[index]; +</xsl:text> + <xsl:text> if(entry == undefined){ +</xsl:text> + <xsl:text> subscriptions[index] = [new Set(), period]; +</xsl:text> + <xsl:text> } else { +</xsl:text> + <xsl:text> entry[1] = period; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>if(has_watchdog){ +</xsl:text> + <xsl:text> // artificially subscribe the watchdog widget to "/heartbeat" hmi variable +</xsl:text> + <xsl:text> // Since dispatch directly calls change_hmi_value, +</xsl:text> + <xsl:text> // PLC will periodically send variable at given frequency +</xsl:text> + <xsl:text> subscribers(heartbeat_index).add({ +</xsl:text> + <xsl:text> /* type: "Watchdog", */ +</xsl:text> + <xsl:text> frequency: 1, +</xsl:text> + <xsl:text> indexes: [heartbeat_index], +</xsl:text> + <xsl:text> new_hmi_value: function(index, value, oldval) { +</xsl:text> + <xsl:text> apply_hmi_value(heartbeat_index, value+1); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> }); +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>var page_fading = "off"; +</xsl:text> + <xsl:text>var page_fading_args = "off"; +</xsl:text> + <xsl:text>function fading_page_switch(...args){ +</xsl:text> + <xsl:text> if(page_fading == "in_progress") +</xsl:text> + <xsl:text> page_fading = "forced"; +</xsl:text> + <xsl:text> else +</xsl:text> + <xsl:text> page_fading = "pending"; +</xsl:text> + <xsl:text> page_fading_args = args; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> requestHMIAnimation(); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text>document.body.style.backgroundColor = "black"; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>// subscribe to per instance current page hmi variable +</xsl:text> + <xsl:text>// PLC must prefix page name with "!" for page switch to happen +</xsl:text> + <xsl:text>subscribers(current_page_var_index).add({ +</xsl:text> + <xsl:text> frequency: 1, +</xsl:text> + <xsl:text> indexes: [current_page_var_index], +</xsl:text> + <xsl:text> new_hmi_value: function(index, value, oldval) { +</xsl:text> + <xsl:text> if(value.startsWith("!")) +</xsl:text> + <xsl:text> fading_page_switch(value.slice(1)); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text>}); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function svg_text_to_multiline(elt) { +</xsl:text> + <xsl:text> return(Array.prototype.map.call(elt.children, x=>x.textContent).join("\n")); +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function multiline_to_svg_text(elt, str, blank) { +</xsl:text> + <xsl:text> str.split('\n').map((line,i) => {elt.children[i].textContent = blank?"":line;}); +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function switch_langnum(langnum) { +</xsl:text> + <xsl:text> langnum = Math.max(0, Math.min(langs.length - 1, langnum)); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> for (let translation of translations) { +</xsl:text> + <xsl:text> let [objs, msgs] = translation; +</xsl:text> + <xsl:text> let msg = msgs[langnum]; +</xsl:text> + <xsl:text> for (let obj of objs) { +</xsl:text> + <xsl:text> multiline_to_svg_text(obj, msg); +</xsl:text> + <xsl:text> obj.setAttribute("lang",langnum); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> return langnum; +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>// backup original texts +</xsl:text> + <xsl:text>for (let translation of translations) { +</xsl:text> + <xsl:text> let [objs, msgs] = translation; +</xsl:text> + <xsl:text> msgs.unshift(svg_text_to_multiline(objs[0])); +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>var lang_local_index = hmi_local_index("lang"); +</xsl:text> + <xsl:text>var langcode_local_index = hmi_local_index("lang_code"); +</xsl:text> + <xsl:text>var langname_local_index = hmi_local_index("lang_name"); +</xsl:text> + <xsl:text>subscribers(lang_local_index).add({ +</xsl:text> + <xsl:text> indexes: [lang_local_index], +</xsl:text> + <xsl:text> new_hmi_value: function(index, value, oldval) { +</xsl:text> + <xsl:text> let current_lang = switch_langnum(value); +</xsl:text> + <xsl:text> let [langname,langcode] = langs[current_lang]; +</xsl:text> + <xsl:text> apply_hmi_value(langcode_local_index, langcode); +</xsl:text> + <xsl:text> apply_hmi_value(langname_local_index, langname); +</xsl:text> + <xsl:text> switch_page(); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text>}); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>// returns en_US, fr_FR or en_UK depending on selected language +</xsl:text> + <xsl:text>function get_current_lang_code(){ +</xsl:text> + <xsl:text> return cache[langcode_local_index]; +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function setup_lang(){ +</xsl:text> + <xsl:text> let current_lang = cache[lang_local_index]; +</xsl:text> + <xsl:text> let new_lang = switch_langnum(current_lang); +</xsl:text> + <xsl:text> if(current_lang != new_lang){ +</xsl:text> + <xsl:text> apply_hmi_value(lang_local_index, new_lang); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>setup_lang(); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function update_subscriptions() { +</xsl:text> + <xsl:text> let delta = []; +</xsl:text> + <xsl:text> for(let index in subscriptions){ +</xsl:text> + <xsl:text> let widgets = subscribers(index); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> // periods are in ms +</xsl:text> + <xsl:text> let previous_period = get_subscription_period(index); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> // subscribing with a zero period is unsubscribing +</xsl:text> + <xsl:text> let new_period = 0; +</xsl:text> + <xsl:text> if(widgets.size > 0) { +</xsl:text> + <xsl:text> let maxfreq = 0; +</xsl:text> + <xsl:text> for(let widget of widgets){ +</xsl:text> + <xsl:text> let wf = widget.frequency; +</xsl:text> + <xsl:text> if(wf != undefined && maxfreq < wf) +</xsl:text> + <xsl:text> maxfreq = wf; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(maxfreq != 0) +</xsl:text> + <xsl:text> new_period = 1000/maxfreq; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(previous_period != new_period) { +</xsl:text> + <xsl:text> set_subscription_period(index, new_period); +</xsl:text> + <xsl:text> if(index <= last_remote_index){ +</xsl:text> + <xsl:text> delta.push( +</xsl:text> + <xsl:text> new Uint8Array([2]), /* subscribe = 2 */ +</xsl:text> + <xsl:text> new Uint32Array([index]), +</xsl:text> + <xsl:text> new Uint16Array([new_period])); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> send_blob(delta); +</xsl:text> + <xsl:text>}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function send_hmi_value(index, value) { +</xsl:text> + <xsl:text> if(index > last_remote_index){ </xsl:text> <xsl:text> dispatch_value(index, value); </xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(persistent_indexes.has(index)){ +</xsl:text> + <xsl:text> let varname = persistent_indexes.get(index); +</xsl:text> + <xsl:text> document.cookie = varname+"="+value+"; max-age=3153600000"; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> return; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> let iectype = hmitree_types[index]; +</xsl:text> + <xsl:text> let tobinary = typedarray_types[iectype]; +</xsl:text> + <xsl:text> send_blob([ +</xsl:text> + <xsl:text> new Uint8Array([0]), /* setval = 0 */ +</xsl:text> + <xsl:text> new Uint32Array([index]), +</xsl:text> + <xsl:text> tobinary(value)]); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf +</xsl:text> + <xsl:text> // cache[index] = value; +</xsl:text> + <xsl:text>}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function apply_hmi_value(index, new_val) { +</xsl:text> + <xsl:text> // Similarly to previous comment, taking decision to update based +</xsl:text> + <xsl:text> // on cache content is bad and can lead to inconsistency +</xsl:text> + <xsl:text> /*let old_val = cache[index];*/ +</xsl:text> + <xsl:text> if(new_val != undefined /*&& old_val != new_val*/) +</xsl:text> + <xsl:text> send_hmi_value(index, new_val); +</xsl:text> + <xsl:text> return new_val; +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>const quotes = {"'":null, '"':null}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function eval_operation_string(old_val, opstr) { +</xsl:text> + <xsl:text> let op = opstr[0]; +</xsl:text> + <xsl:text> let given_val; +</xsl:text> + <xsl:text> if(opstr.length < 2) +</xsl:text> + <xsl:text> return undefined; +</xsl:text> + <xsl:text> if(opstr[1] in quotes){ +</xsl:text> + <xsl:text> if(opstr.length < 3) +</xsl:text> + <xsl:text> return undefined; +</xsl:text> + <xsl:text> if(opstr[opstr.length-1] == opstr[1]){ +</xsl:text> + <xsl:text> given_val = opstr.slice(2,opstr.length-1); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } else { +</xsl:text> + <xsl:text> given_val = Number(opstr.slice(1)); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> let new_val; +</xsl:text> + <xsl:text> switch(op){ +</xsl:text> + <xsl:text> case "=": +</xsl:text> + <xsl:text> new_val = given_val; +</xsl:text> + <xsl:text> break; +</xsl:text> + <xsl:text> case "+": +</xsl:text> + <xsl:text> new_val = old_val + given_val; +</xsl:text> + <xsl:text> break; +</xsl:text> + <xsl:text> case "-": +</xsl:text> + <xsl:text> new_val = old_val - given_val; +</xsl:text> + <xsl:text> break; +</xsl:text> + <xsl:text> case "*": +</xsl:text> + <xsl:text> new_val = old_val * given_val; +</xsl:text> + <xsl:text> break; +</xsl:text> + <xsl:text> case "/": +</xsl:text> + <xsl:text> new_val = old_val / given_val; +</xsl:text> + <xsl:text> break; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> return new_val; +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>var current_visible_page; +</xsl:text> + <xsl:text>var current_subscribed_page; +</xsl:text> + <xsl:text>var current_page_index; +</xsl:text> + <xsl:text>var page_node_local_index = hmi_local_index("page_node"); +</xsl:text> + <xsl:text>var page_switch_in_progress = false; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function toggleFullscreen() { +</xsl:text> + <xsl:text> let elem = document.documentElement; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if (!document.fullscreenElement) { +</xsl:text> + <xsl:text> elem.requestFullscreen().catch(err => { +</xsl:text> + <xsl:text> console.log("Error attempting to enable full-screen mode: "+err.message+" ("+err.name+")"); +</xsl:text> <xsl:text> }); </xsl:text> - <xsl:text> updates.clear(); + <xsl:text> } else { +</xsl:text> + <xsl:text> document.exitFullscreen(); +</xsl:text> + <xsl:text> } </xsl:text> <xsl:text>} </xsl:text> <xsl:text> </xsl:text> - <xsl:text>// Called on requestAnimationFrame, modifies DOM -</xsl:text> - <xsl:text>var requestAnimationFrameID = null; -</xsl:text> - <xsl:text>function animate() { -</xsl:text> - <xsl:text> let rearm = true; -</xsl:text> - <xsl:text> do{ -</xsl:text> - <xsl:text> if(page_fading == "pending" || page_fading == "forced"){ -</xsl:text> - <xsl:text> if(page_fading == "pending") -</xsl:text> - <xsl:text> svg_root.classList.add("fade-out-page"); -</xsl:text> - <xsl:text> page_fading = "in_progress"; -</xsl:text> - <xsl:text> if(page_fading_args.length) -</xsl:text> - <xsl:text> setTimeout(function(){ -</xsl:text> - <xsl:text> switch_page(...page_fading_args); -</xsl:text> - <xsl:text> },1); -</xsl:text> - <xsl:text> break; + <xsl:text>function prepare_svg() { +</xsl:text> + <xsl:text> // prevents context menu from appearing on right click and long touch +</xsl:text> + <xsl:text> document.body.addEventListener('contextmenu', e => { +</xsl:text> + <xsl:text> toggleFullscreen(); +</xsl:text> + <xsl:text> e.preventDefault(); +</xsl:text> + <xsl:text> }); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> for(let eltid in detachable_elements){ +</xsl:text> + <xsl:text> let [element,parent] = detachable_elements[eltid]; +</xsl:text> + <xsl:text> parent.removeChild(element); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text>}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function switch_page(page_name, page_index) { +</xsl:text> + <xsl:text> if(page_switch_in_progress){ +</xsl:text> + <xsl:text> /* page switch already going */ +</xsl:text> + <xsl:text> /* TODO LOG ERROR */ +</xsl:text> + <xsl:text> return false; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> page_switch_in_progress = true; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(page_name == undefined) +</xsl:text> + <xsl:text> page_name = current_subscribed_page; +</xsl:text> + <xsl:text> else if(page_index == undefined){ +</xsl:text> + <xsl:text> [page_name, page_index] = page_name.split('@') +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> let old_desc = page_desc[current_subscribed_page]; +</xsl:text> + <xsl:text> let new_desc = page_desc[page_name]; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(new_desc == undefined){ +</xsl:text> + <xsl:text> /* TODO LOG ERROR */ +</xsl:text> + <xsl:text> return false; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(page_index == undefined) +</xsl:text> + <xsl:text> page_index = new_desc.page_index; +</xsl:text> + <xsl:text> else if(typeof(page_index) == "string") { +</xsl:text> + <xsl:text> let hmitree_node = hmitree_nodes[page_index]; +</xsl:text> + <xsl:text> if(hmitree_node !== undefined){ +</xsl:text> + <xsl:text> let [int_index, hmiclass] = hmitree_node; +</xsl:text> + <xsl:text> if(hmiclass == new_desc.page_class) +</xsl:text> + <xsl:text> page_index = int_index; +</xsl:text> + <xsl:text> else +</xsl:text> + <xsl:text> page_index = new_desc.page_index; +</xsl:text> + <xsl:text> } else { +</xsl:text> + <xsl:text> page_index = new_desc.page_index; </xsl:text> <xsl:text> } </xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> // Do the page swith if pending -</xsl:text> - <xsl:text> if(page_switch_in_progress){ -</xsl:text> - <xsl:text> if(current_subscribed_page != current_visible_page){ -</xsl:text> - <xsl:text> switch_visible_page(current_subscribed_page); + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(old_desc){ +</xsl:text> + <xsl:text> old_desc.widgets.map(([widget,relativeness])=>widget.unsub()); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> const new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> const container_id = page_name + (page_index != undefined ? page_index : ""); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness,container_id)); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> update_subscriptions(); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> current_subscribed_page = page_name; +</xsl:text> + <xsl:text> current_page_index = page_index; +</xsl:text> + <xsl:text> let page_node; +</xsl:text> + <xsl:text> if(page_index != undefined){ +</xsl:text> + <xsl:text> page_node = hmitree_paths[page_index]; +</xsl:text> + <xsl:text> }else{ +</xsl:text> + <xsl:text> page_node = ""; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> apply_hmi_value(page_node_local_index, page_node); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> jumps_need_update = true; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> requestHMIAnimation(); +</xsl:text> + <xsl:text> jump_history.push([page_name, page_index]); +</xsl:text> + <xsl:text> if(jump_history.length > 42) +</xsl:text> + <xsl:text> jump_history.shift(); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> apply_hmi_value(current_page_var_index, page_index == undefined +</xsl:text> + <xsl:text> ? page_name +</xsl:text> + <xsl:text> : page_name + "@" + hmitree_paths[page_index]); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> return true; +</xsl:text> + <xsl:text>}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function switch_visible_page(page_name) { +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> let old_desc = page_desc[current_visible_page]; +</xsl:text> + <xsl:text> let new_desc = page_desc[page_name]; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(old_desc){ +</xsl:text> + <xsl:text> for(let eltid in old_desc.required_detachables){ +</xsl:text> + <xsl:text> if(!(eltid in new_desc.required_detachables)){ +</xsl:text> + <xsl:text> let [element, parent] = old_desc.required_detachables[eltid]; +</xsl:text> + <xsl:text> parent.removeChild(element); </xsl:text> <xsl:text> } </xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> page_switch_in_progress = false; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(page_fading == "in_progress"){ -</xsl:text> - <xsl:text> svg_root.classList.remove("fade-out-page"); -</xsl:text> - <xsl:text> page_fading = "off"; + <xsl:text> } +</xsl:text> + <xsl:text> for(let eltid in new_desc.required_detachables){ +</xsl:text> + <xsl:text> if(!(eltid in old_desc.required_detachables)){ +</xsl:text> + <xsl:text> let [element, parent] = new_desc.required_detachables[eltid]; +</xsl:text> + <xsl:text> parent.appendChild(element); </xsl:text> <xsl:text> } </xsl:text> <xsl:text> } </xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(jumps_need_update) update_jumps(); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> pending_widget_animates.forEach(widget => widget._animate()); -</xsl:text> - <xsl:text> pending_widget_animates = []; -</xsl:text> - <xsl:text> rearm = false; -</xsl:text> - <xsl:text> } while(0); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> requestAnimationFrameID = null; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(rearm) requestHMIAnimation(); + <xsl:text> }else{ +</xsl:text> + <xsl:text> for(let eltid in new_desc.required_detachables){ +</xsl:text> + <xsl:text> let [element, parent] = new_desc.required_detachables[eltid]; +</xsl:text> + <xsl:text> parent.appendChild(element); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> svg_root.setAttribute('viewBox',new_desc.bbox.join(" ")); +</xsl:text> + <xsl:text> current_visible_page = page_name; +</xsl:text> + <xsl:text>}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>/* From https://jsfiddle.net/ibowankenobi/1mmh7rs6/6/ */ +</xsl:text> + <xsl:text>function getAbsoluteCTM(element){ +</xsl:text> + <xsl:text> var height = svg_root.height.baseVal.value, +</xsl:text> + <xsl:text> width = svg_root.width.baseVal.value, +</xsl:text> + <xsl:text> viewBoxRect = svg_root.viewBox.baseVal, +</xsl:text> + <xsl:text> vHeight = viewBoxRect.height, +</xsl:text> + <xsl:text> vWidth = viewBoxRect.width; +</xsl:text> + <xsl:text> if(!vWidth || !vHeight){ +</xsl:text> + <xsl:text> return element.getCTM(); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> var sH = height/vHeight, +</xsl:text> + <xsl:text> sW = width/vWidth, +</xsl:text> + <xsl:text> matrix = svg_root.createSVGMatrix(); +</xsl:text> + <xsl:text> matrix.a = sW; +</xsl:text> + <xsl:text> matrix.d = sH +</xsl:text> + <xsl:text> var realCTM = element.getCTM().multiply(matrix.inverse()); +</xsl:text> + <xsl:text> realCTM.e = realCTM.e/sW + viewBoxRect.x; +</xsl:text> + <xsl:text> realCTM.f = realCTM.f/sH + viewBoxRect.y; +</xsl:text> + <xsl:text> return realCTM; </xsl:text> <xsl:text>} </xsl:text> <xsl:text> </xsl:text> - <xsl:text>function requestHMIAnimation() { -</xsl:text> - <xsl:text> if(requestAnimationFrameID == null){ -</xsl:text> - <xsl:text> requestAnimationFrameID = window.requestAnimationFrame(animate); -</xsl:text> - <xsl:text> } + <xsl:text>function apply_reference_frames(){ +</xsl:text> + <xsl:text> const matches = svg_root.querySelectorAll("g[svghmi_x_offset]"); +</xsl:text> + <xsl:text> matches.forEach((group) => { +</xsl:text> + <xsl:text> let [x,y] = ["x", "y"].map((axis) => Number(group.getAttribute("svghmi_"+axis+"_offset"))); +</xsl:text> + <xsl:text> let ctm = getAbsoluteCTM(group); +</xsl:text> + <xsl:text> // zero translation part of CTM +</xsl:text> + <xsl:text> // to only apply rotation/skewing to offset vector +</xsl:text> + <xsl:text> ctm.e = 0; +</xsl:text> + <xsl:text> ctm.f = 0; +</xsl:text> + <xsl:text> let invctm = ctm.inverse(); +</xsl:text> + <xsl:text> let vect = new DOMPoint(x, y); +</xsl:text> + <xsl:text> let newvect = vect.matrixTransform(invctm); +</xsl:text> + <xsl:text> let transform = svg_root.createSVGTransform(); +</xsl:text> + <xsl:text> transform.setTranslate(newvect.x, newvect.y); +</xsl:text> + <xsl:text> group.transform.baseVal.appendItem(transform); +</xsl:text> + <xsl:text> ["x", "y"].forEach((axis) => group.removeAttribute("svghmi_"+axis+"_offset")); +</xsl:text> + <xsl:text> }); </xsl:text> <xsl:text>} </xsl:text> <xsl:text> </xsl:text> - <xsl:text>// Message reception handler -</xsl:text> - <xsl:text>// Hash is verified and HMI values updates resulting from binary parsing -</xsl:text> - <xsl:text>// are stored until browser can compute next frame, DOM is left untouched -</xsl:text> - <xsl:text>ws.onmessage = function (evt) { -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> let data = evt.data; -</xsl:text> - <xsl:text> let dv = new DataView(data); -</xsl:text> - <xsl:text> let i = 0; -</xsl:text> - <xsl:text> try { -</xsl:text> - <xsl:text> for(let hash_int of hmi_hash) { -</xsl:text> - <xsl:text> if(hash_int != dv.getUint8(i)){ -</xsl:text> - <xsl:text> throw new Error("Hash doesn't match"); -</xsl:text> - <xsl:text> }; -</xsl:text> - <xsl:text> i++; -</xsl:text> - <xsl:text> }; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> while(i < data.byteLength){ -</xsl:text> - <xsl:text> let index = dv.getUint32(i, true); -</xsl:text> - <xsl:text> i += 4; -</xsl:text> - <xsl:text> let iectype = hmitree_types[index]; -</xsl:text> - <xsl:text> if(iectype != undefined){ -</xsl:text> - <xsl:text> let dvgetter = dvgetters[iectype]; -</xsl:text> - <xsl:text> let [value, bytesize] = dvgetter(dv,i); -</xsl:text> - <xsl:text> updates.set(index, value); -</xsl:text> - <xsl:text> i += bytesize; -</xsl:text> - <xsl:text> } else { -</xsl:text> - <xsl:text> throw new Error("Unknown index "+index); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> }; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> apply_updates(); -</xsl:text> - <xsl:text> // register for rendering on next frame, since there are updates -</xsl:text> - <xsl:text> } catch(err) { -</xsl:text> - <xsl:text> // 1003 is for "Unsupported Data" -</xsl:text> - <xsl:text> // ws.close(1003, err.message); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> // TODO : remove debug alert ? -</xsl:text> - <xsl:text> alert("Error : "+err.message+"\nHMI will be reloaded."); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> // force reload ignoring cache -</xsl:text> - <xsl:text> location.reload(true); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text>}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>hmi_hash_u8 = new Uint8Array(hmi_hash); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function send_blob(data) { -</xsl:text> - <xsl:text> if(data.length > 0) { -</xsl:text> - <xsl:text> ws.send(new Blob([hmi_hash_u8].concat(data))); -</xsl:text> - <xsl:text> }; -</xsl:text> - <xsl:text>}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>const typedarray_types = { -</xsl:text> - <xsl:text> INT: (number) => new Int16Array([number]), -</xsl:text> - <xsl:text> BOOL: (truth) => new Int16Array([truth]), -</xsl:text> - <xsl:text> NODE: (truth) => new Int16Array([truth]), -</xsl:text> - <xsl:text> REAL: (number) => new Float32Array([number]), -</xsl:text> - <xsl:text> STRING: (str) => { -</xsl:text> - <xsl:text> // beremiz default string max size is 128 -</xsl:text> - <xsl:text> str = str.slice(0,128); -</xsl:text> - <xsl:text> binary = new Uint8Array(str.length + 1); -</xsl:text> - <xsl:text> binary[0] = str.length; -</xsl:text> - <xsl:text> for(let i = 0; i < str.length; i++){ -</xsl:text> - <xsl:text> binary[i+1] = str.charCodeAt(i); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> return binary; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> /* TODO */ -</xsl:text> - <xsl:text>}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function send_reset() { -</xsl:text> - <xsl:text> send_blob(new Uint8Array([1])); /* reset = 1 */ -</xsl:text> - <xsl:text>}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>var subscriptions = []; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function subscribers(index) { -</xsl:text> - <xsl:text> let entry = subscriptions[index]; -</xsl:text> - <xsl:text> let res; -</xsl:text> - <xsl:text> if(entry == undefined){ -</xsl:text> - <xsl:text> res = new Set(); -</xsl:text> - <xsl:text> subscriptions[index] = [res,0]; -</xsl:text> - <xsl:text> }else{ -</xsl:text> - <xsl:text> [res, _ign] = entry; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> return res -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function get_subscription_period(index) { -</xsl:text> - <xsl:text> let entry = subscriptions[index]; -</xsl:text> - <xsl:text> if(entry == undefined) -</xsl:text> - <xsl:text> return 0; -</xsl:text> - <xsl:text> let [_ign, period] = entry; -</xsl:text> - <xsl:text> return period; -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function set_subscription_period(index, period) { -</xsl:text> - <xsl:text> let entry = subscriptions[index]; -</xsl:text> - <xsl:text> if(entry == undefined){ -</xsl:text> - <xsl:text> subscriptions[index] = [new Set(), period]; -</xsl:text> - <xsl:text> } else { -</xsl:text> - <xsl:text> entry[1] = period; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>if(has_watchdog){ -</xsl:text> - <xsl:text> // artificially subscribe the watchdog widget to "/heartbeat" hmi variable -</xsl:text> - <xsl:text> // Since dispatch directly calls change_hmi_value, -</xsl:text> - <xsl:text> // PLC will periodically send variable at given frequency -</xsl:text> - <xsl:text> subscribers(heartbeat_index).add({ -</xsl:text> - <xsl:text> /* type: "Watchdog", */ -</xsl:text> - <xsl:text> frequency: 1, -</xsl:text> - <xsl:text> indexes: [heartbeat_index], -</xsl:text> - <xsl:text> new_hmi_value: function(index, value, oldval) { -</xsl:text> - <xsl:text> apply_hmi_value(heartbeat_index, value+1); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> }); -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>var page_fading = "off"; -</xsl:text> - <xsl:text>var page_fading_args = "off"; -</xsl:text> - <xsl:text>function fading_page_switch(...args){ -</xsl:text> - <xsl:text> if(page_fading == "in_progress") -</xsl:text> - <xsl:text> page_fading = "forced"; -</xsl:text> - <xsl:text> else -</xsl:text> - <xsl:text> page_fading = "pending"; -</xsl:text> - <xsl:text> page_fading_args = args; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> requestHMIAnimation(); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text>document.body.style.backgroundColor = "black"; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>// subscribe to per instance current page hmi variable -</xsl:text> - <xsl:text>// PLC must prefix page name with "!" for page switch to happen -</xsl:text> - <xsl:text>subscribers(current_page_var_index).add({ -</xsl:text> - <xsl:text> frequency: 1, -</xsl:text> - <xsl:text> indexes: [current_page_var_index], -</xsl:text> - <xsl:text> new_hmi_value: function(index, value, oldval) { -</xsl:text> - <xsl:text> if(value.startsWith("!")) -</xsl:text> - <xsl:text> fading_page_switch(value.slice(1)); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text>}); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function svg_text_to_multiline(elt) { -</xsl:text> - <xsl:text> return(Array.prototype.map.call(elt.children, x=>x.textContent).join("\n")); -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function multiline_to_svg_text(elt, str, blank) { -</xsl:text> - <xsl:text> str.split('\n').map((line,i) => {elt.children[i].textContent = blank?"":line;}); -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function switch_langnum(langnum) { -</xsl:text> - <xsl:text> langnum = Math.max(0, Math.min(langs.length - 1, langnum)); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> for (let translation of translations) { -</xsl:text> - <xsl:text> let [objs, msgs] = translation; -</xsl:text> - <xsl:text> let msg = msgs[langnum]; -</xsl:text> - <xsl:text> for (let obj of objs) { -</xsl:text> - <xsl:text> multiline_to_svg_text(obj, msg); -</xsl:text> - <xsl:text> obj.setAttribute("lang",langnum); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> return langnum; -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>// backup original texts -</xsl:text> - <xsl:text>for (let translation of translations) { -</xsl:text> - <xsl:text> let [objs, msgs] = translation; -</xsl:text> - <xsl:text> msgs.unshift(svg_text_to_multiline(objs[0])); -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>var lang_local_index = hmi_local_index("lang"); -</xsl:text> - <xsl:text>var langcode_local_index = hmi_local_index("lang_code"); -</xsl:text> - <xsl:text>var langname_local_index = hmi_local_index("lang_name"); -</xsl:text> - <xsl:text>subscribers(lang_local_index).add({ -</xsl:text> - <xsl:text> indexes: [lang_local_index], -</xsl:text> - <xsl:text> new_hmi_value: function(index, value, oldval) { -</xsl:text> - <xsl:text> let current_lang = switch_langnum(value); -</xsl:text> - <xsl:text> let [langname,langcode] = langs[current_lang]; -</xsl:text> - <xsl:text> apply_hmi_value(langcode_local_index, langcode); -</xsl:text> - <xsl:text> apply_hmi_value(langname_local_index, langname); -</xsl:text> - <xsl:text> switch_page(); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text>}); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>// returns en_US, fr_FR or en_UK depending on selected language -</xsl:text> - <xsl:text>function get_current_lang_code(){ -</xsl:text> - <xsl:text> return cache[langcode_local_index]; -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function setup_lang(){ -</xsl:text> - <xsl:text> let current_lang = cache[lang_local_index]; -</xsl:text> - <xsl:text> let new_lang = switch_langnum(current_lang); -</xsl:text> - <xsl:text> if(current_lang != new_lang){ -</xsl:text> - <xsl:text> apply_hmi_value(lang_local_index, new_lang); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>setup_lang(); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function update_subscriptions() { -</xsl:text> - <xsl:text> let delta = []; -</xsl:text> - <xsl:text> for(let index in subscriptions){ -</xsl:text> - <xsl:text> let widgets = subscribers(index); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> // periods are in ms -</xsl:text> - <xsl:text> let previous_period = get_subscription_period(index); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> // subscribing with a zero period is unsubscribing -</xsl:text> - <xsl:text> let new_period = 0; -</xsl:text> - <xsl:text> if(widgets.size > 0) { -</xsl:text> - <xsl:text> let maxfreq = 0; -</xsl:text> - <xsl:text> for(let widget of widgets){ -</xsl:text> - <xsl:text> let wf = widget.frequency; -</xsl:text> - <xsl:text> if(wf != undefined && maxfreq < wf) -</xsl:text> - <xsl:text> maxfreq = wf; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(maxfreq != 0) -</xsl:text> - <xsl:text> new_period = 1000/maxfreq; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(previous_period != new_period) { -</xsl:text> - <xsl:text> set_subscription_period(index, new_period); -</xsl:text> - <xsl:text> if(index <= last_remote_index){ -</xsl:text> - <xsl:text> delta.push( -</xsl:text> - <xsl:text> new Uint8Array([2]), /* subscribe = 2 */ -</xsl:text> - <xsl:text> new Uint32Array([index]), -</xsl:text> - <xsl:text> new Uint16Array([new_period])); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> send_blob(delta); -</xsl:text> - <xsl:text>}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function send_hmi_value(index, value) { -</xsl:text> - <xsl:text> if(index > last_remote_index){ -</xsl:text> - <xsl:text> dispatch_value(index, value); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(persistent_indexes.has(index)){ -</xsl:text> - <xsl:text> let varname = persistent_indexes.get(index); -</xsl:text> - <xsl:text> document.cookie = varname+"="+value+"; max-age=3153600000"; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> return; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> let iectype = hmitree_types[index]; -</xsl:text> - <xsl:text> let tobinary = typedarray_types[iectype]; -</xsl:text> - <xsl:text> send_blob([ -</xsl:text> - <xsl:text> new Uint8Array([0]), /* setval = 0 */ -</xsl:text> - <xsl:text> new Uint32Array([index]), -</xsl:text> - <xsl:text> tobinary(value)]); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf -</xsl:text> - <xsl:text> // cache[index] = value; -</xsl:text> - <xsl:text>}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function apply_hmi_value(index, new_val) { -</xsl:text> - <xsl:text> // Similarly to previous comment, taking decision to update based -</xsl:text> - <xsl:text> // on cache content is bad and can lead to inconsistency -</xsl:text> - <xsl:text> /*let old_val = cache[index];*/ -</xsl:text> - <xsl:text> if(new_val != undefined /*&& old_val != new_val*/) -</xsl:text> - <xsl:text> send_hmi_value(index, new_val); -</xsl:text> - <xsl:text> return new_val; -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>const quotes = {"'":null, '"':null}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function eval_operation_string(old_val, opstr) { -</xsl:text> - <xsl:text> let op = opstr[0]; -</xsl:text> - <xsl:text> let given_val; -</xsl:text> - <xsl:text> if(opstr.length < 2) -</xsl:text> - <xsl:text> return undefined; -</xsl:text> - <xsl:text> if(opstr[1] in quotes){ -</xsl:text> - <xsl:text> if(opstr.length < 3) -</xsl:text> - <xsl:text> return undefined; -</xsl:text> - <xsl:text> if(opstr[opstr.length-1] == opstr[1]){ -</xsl:text> - <xsl:text> given_val = opstr.slice(2,opstr.length-1); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } else { -</xsl:text> - <xsl:text> given_val = Number(opstr.slice(1)); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> let new_val; -</xsl:text> - <xsl:text> switch(op){ -</xsl:text> - <xsl:text> case "=": -</xsl:text> - <xsl:text> new_val = given_val; -</xsl:text> - <xsl:text> break; -</xsl:text> - <xsl:text> case "+": -</xsl:text> - <xsl:text> new_val = old_val + given_val; -</xsl:text> - <xsl:text> break; -</xsl:text> - <xsl:text> case "-": -</xsl:text> - <xsl:text> new_val = old_val - given_val; -</xsl:text> - <xsl:text> break; -</xsl:text> - <xsl:text> case "*": -</xsl:text> - <xsl:text> new_val = old_val * given_val; -</xsl:text> - <xsl:text> break; -</xsl:text> - <xsl:text> case "/": -</xsl:text> - <xsl:text> new_val = old_val / given_val; -</xsl:text> - <xsl:text> break; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> return new_val; -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>var current_visible_page; -</xsl:text> - <xsl:text>var current_subscribed_page; -</xsl:text> - <xsl:text>var current_page_index; -</xsl:text> - <xsl:text>var page_node_local_index = hmi_local_index("page_node"); -</xsl:text> - <xsl:text>var page_switch_in_progress = false; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function toggleFullscreen() { -</xsl:text> - <xsl:text> let elem = document.documentElement; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if (!document.fullscreenElement) { -</xsl:text> - <xsl:text> elem.requestFullscreen().catch(err => { -</xsl:text> - <xsl:text> console.log("Error attempting to enable full-screen mode: "+err.message+" ("+err.name+")"); -</xsl:text> - <xsl:text> }); -</xsl:text> - <xsl:text> } else { -</xsl:text> - <xsl:text> document.exitFullscreen(); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function prepare_svg() { -</xsl:text> - <xsl:text> // prevents context menu from appearing on right click and long touch -</xsl:text> - <xsl:text> document.body.addEventListener('contextmenu', e => { -</xsl:text> - <xsl:text> toggleFullscreen(); -</xsl:text> - <xsl:text> e.preventDefault(); -</xsl:text> - <xsl:text> }); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> for(let eltid in detachable_elements){ -</xsl:text> - <xsl:text> let [element,parent] = detachable_elements[eltid]; -</xsl:text> - <xsl:text> parent.removeChild(element); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text>}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function switch_page(page_name, page_index) { -</xsl:text> - <xsl:text> if(page_switch_in_progress){ -</xsl:text> - <xsl:text> /* page switch already going */ -</xsl:text> - <xsl:text> /* TODO LOG ERROR */ -</xsl:text> - <xsl:text> return false; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> page_switch_in_progress = true; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(page_name == undefined) -</xsl:text> - <xsl:text> page_name = current_subscribed_page; -</xsl:text> - <xsl:text> else if(page_index == undefined){ -</xsl:text> - <xsl:text> [page_name, page_index] = page_name.split('@') -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> let old_desc = page_desc[current_subscribed_page]; -</xsl:text> - <xsl:text> let new_desc = page_desc[page_name]; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(new_desc == undefined){ -</xsl:text> - <xsl:text> /* TODO LOG ERROR */ -</xsl:text> - <xsl:text> return false; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(page_index == undefined) -</xsl:text> - <xsl:text> page_index = new_desc.page_index; -</xsl:text> - <xsl:text> else if(typeof(page_index) == "string") { -</xsl:text> - <xsl:text> let hmitree_node = hmitree_nodes[page_index]; -</xsl:text> - <xsl:text> if(hmitree_node !== undefined){ -</xsl:text> - <xsl:text> let [int_index, hmiclass] = hmitree_node; -</xsl:text> - <xsl:text> if(hmiclass == new_desc.page_class) -</xsl:text> - <xsl:text> page_index = int_index; -</xsl:text> - <xsl:text> else -</xsl:text> - <xsl:text> page_index = new_desc.page_index; -</xsl:text> - <xsl:text> } else { -</xsl:text> - <xsl:text> page_index = new_desc.page_index; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(old_desc){ -</xsl:text> - <xsl:text> old_desc.widgets.map(([widget,relativeness])=>widget.unsub()); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> const new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> const container_id = page_name + (page_index != undefined ? page_index : ""); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness,container_id)); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> update_subscriptions(); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> current_subscribed_page = page_name; -</xsl:text> - <xsl:text> current_page_index = page_index; -</xsl:text> - <xsl:text> let page_node; -</xsl:text> - <xsl:text> if(page_index != undefined){ -</xsl:text> - <xsl:text> page_node = hmitree_paths[page_index]; -</xsl:text> - <xsl:text> }else{ -</xsl:text> - <xsl:text> page_node = ""; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> apply_hmi_value(page_node_local_index, page_node); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> jumps_need_update = true; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> requestHMIAnimation(); -</xsl:text> - <xsl:text> jump_history.push([page_name, page_index]); -</xsl:text> - <xsl:text> if(jump_history.length > 42) -</xsl:text> - <xsl:text> jump_history.shift(); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> apply_hmi_value(current_page_var_index, page_index == undefined -</xsl:text> - <xsl:text> ? page_name -</xsl:text> - <xsl:text> : page_name + "@" + hmitree_paths[page_index]); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> return true; -</xsl:text> - <xsl:text>}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function switch_visible_page(page_name) { -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> let old_desc = page_desc[current_visible_page]; -</xsl:text> - <xsl:text> let new_desc = page_desc[page_name]; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(old_desc){ -</xsl:text> - <xsl:text> for(let eltid in old_desc.required_detachables){ -</xsl:text> - <xsl:text> if(!(eltid in new_desc.required_detachables)){ -</xsl:text> - <xsl:text> let [element, parent] = old_desc.required_detachables[eltid]; -</xsl:text> - <xsl:text> parent.removeChild(element); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> for(let eltid in new_desc.required_detachables){ -</xsl:text> - <xsl:text> if(!(eltid in old_desc.required_detachables)){ -</xsl:text> - <xsl:text> let [element, parent] = new_desc.required_detachables[eltid]; -</xsl:text> - <xsl:text> parent.appendChild(element); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> }else{ -</xsl:text> - <xsl:text> for(let eltid in new_desc.required_detachables){ -</xsl:text> - <xsl:text> let [element, parent] = new_desc.required_detachables[eltid]; -</xsl:text> - <xsl:text> parent.appendChild(element); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> svg_root.setAttribute('viewBox',new_desc.bbox.join(" ")); -</xsl:text> - <xsl:text> current_visible_page = page_name; -</xsl:text> - <xsl:text>}; -</xsl:text> - <xsl:text> -</xsl:text> <xsl:text>// Once connection established </xsl:text> <xsl:text>ws.onopen = function (evt) { </xsl:text> + <xsl:text> apply_reference_frames(); +</xsl:text> <xsl:text> init_widgets(); </xsl:text> <xsl:text> send_reset();