SVGHMI: add support for custom widgets and own SVG widget library.
--- a/LPCSVGHMI.py Wed May 28 15:31:26 2025 +0200
+++ b/LPCSVGHMI.py Thu May 29 15:56:09 2025 +0200
@@ -1,4 +1,5 @@
from svghmi.svghmi import SVGHMI, SVGHMILibrary, paths
@@ -107,11 +108,13 @@
SVGHMI.getDefaultSVG = getDefaultSVG
-def getDefaultSVGLibrary():
ScriptDirectory = paths.AbsDir(__file__)
- return os.path.join(ScriptDirectory, "LPCSVGHMILibrary")
+ return os.path.join(ScriptDirectory, "LPCSVGHMI") -ui.default_libdir = getDefaultSVGLibrary()
+svghmi.svghmi.ScriptDirectory = getLPCSVGHMI_dir() +ui.ScriptDirectory = getLPCSVGHMI_dir() +ui.default_libdir = os.path.join(getLPCSVGHMI_dir(), "widgetlib") old_Generate_C = SVGHMILibrary.Generate_C
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/LPCSVGHMI/Makefile Thu May 29 15:56:09 2025 +0200
@@ -0,0 +1,16 @@
+our_lib_path = $(abspath .) +original_svghmi_path = $(abspath ../../beremiz/svghmi) +our_widgets := $(wildcard *.ysl2) +xsltfiles := gen_index_xhtml.xslt gen_dnd_widget_svg.xslt analyse_widget.xslt +$(xsltfiles): $(our_widgets) + for f in $(xsltfiles); do rm -f $(original_svghmi_path)/$$f; done + YML_PATH=$(original_svghmi_path):$(our_lib_path) $(MAKE) -C $(original_svghmi_path) + for f in $(xsltfiles); do cp $(original_svghmi_path)/$$f $(our_lib_path)/$$f; done --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/LPCSVGHMI/analyse_widget.xslt Thu May 29 15:56:09 2025 +0200
@@ -0,0 +1,1224 @@
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="http://exslt.org/common" xmlns:regexp="http://exslt.org/regular-expressions" xmlns:str="http://exslt.org/strings" xmlns:func="http://exslt.org/functions" xmlns:svg="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" version="1.0" extension-element-prefixes="ns func exsl regexp str dyn" exclude-result-prefixes="ns func exsl regexp str dyn svg inkscape"> + <xsl:output method="xml"/> + <xsl:variable name="indexed_hmitree" select="/.."/> + <xsl:variable name="pathregex" select="'^(\w+=)?([^,=]+)([-.\w,]*)$'"/> + <xsl:variable name="newline"> + <xsl:variable name="twonewlines" select="concat($newline,$newline)"/> + <xsl:template mode="parselabel" match="*"> + <xsl:variable name="label" select="@inkscape:label"/> + <xsl:variable name="desc" select="svg:desc"/> + <xsl:variable name="len" select="string-length($label)"/> + <xsl:variable name="has_continuation" select="substring($label,$len,1)='\'"/> + <xsl:variable name="full_decl"> + <xsl:when test="$has_continuation"> + <xsl:variable name="_continuation" select="substring-before($desc, $twonewlines)"/> + <xsl:variable name="continuation"> + <xsl:when test="$_continuation"> + <xsl:value-of select="$_continuation"/> + <xsl:value-of select="$desc"/> + <xsl:value-of select="concat(substring($label,1,$len - 1),translate($continuation,$newline,''))"/> + <xsl:value-of select="$label"/> + <xsl:variable name="id" select="@id"/> + <xsl:variable name="declaration" select="substring-after($full_decl,'HMI:')"/> + <xsl:variable name="_args" select="substring-before($declaration,'@')"/> + <xsl:variable name="args"> + <xsl:when test="$_args"> + <xsl:value-of select="$_args"/> + <xsl:value-of select="$declaration"/> + <xsl:variable name="_typefreq" select="substring-before($args,':')"/> + <xsl:variable name="typefreq"> + <xsl:when test="$_typefreq"> + <xsl:value-of select="$_typefreq"/> + <xsl:value-of select="$args"/> + <xsl:variable name="freq" select="substring-after($typefreq,'|')"/> + <xsl:variable name="_type" select="substring-before($typefreq,'|')"/> + <xsl:variable name="type"> + <xsl:when test="$_type"> + <xsl:value-of select="$_type"/> + <xsl:value-of select="$typefreq"/> + <xsl:attribute name="id"> + <xsl:value-of select="$id"/> + <xsl:attribute name="type"> + <xsl:value-of select="$type"/> + <xsl:if test="not(regexp:test($freq,'^[0-9]*(\.[0-9]+)?[smh]?'))"> + <xsl:message terminate="yes"> + <xsl:text>Widget id:</xsl:text> + <xsl:value-of select="$id"/> + <xsl:text> label:</xsl:text> + <xsl:value-of select="$full_decl"/> + <xsl:text> has wrong syntax of frequency forcing </xsl:text> + <xsl:value-of select="$freq"/> + <xsl:attribute name="freq"> + <xsl:value-of select="$freq"/> + <xsl:variable name="tail" select="substring-after($declaration,'@')"/> + <xsl:variable name="taillen" select="string-length($tail)"/> + <xsl:variable name="has_enable" select="contains($tail, '#')"/> + <xsl:variable name="paths"> + <xsl:when test="$has_enable"> + <xsl:value-of select="substring-before($tail,'#')"/> + <xsl:value-of select="$tail"/> + <xsl:if test="$has_enable"> + <xsl:variable name="enable_expr" select="substring-after($tail,'#')"/> + <xsl:attribute name="enable_expr"> + <xsl:value-of select="$enable_expr"/> + <xsl:for-each select="str:split(substring-after($args, ':'), ':')"> + <xsl:attribute name="value"> + <xsl:value-of select="."/> + <xsl:for-each select="str:split($paths, '@')"> + <xsl:if test="string-length(.) > 0"> + <xsl:variable name="path_match" select="regexp:match(.,$pathregex)"/> + <xsl:variable name="pathassign" select="substring-before($path_match[2],'=')"/> + <xsl:variable name="pathminmax" select="str:split($path_match[4],',')"/> + <xsl:variable name="path" select="$path_match[3]"/> + <xsl:variable name="pathminmaxcount" select="count($pathminmax)"/> + <xsl:if test="not($path)"> + <xsl:message terminate="yes"> + <xsl:text>Widget id:</xsl:text> + <xsl:value-of select="$id"/> + <xsl:text> label:</xsl:text> + <xsl:value-of select="$full_decl"/> + <xsl:text> has wrong syntax</xsl:text> + <xsl:attribute name="value"> + <xsl:value-of select="$path"/> + <xsl:if test="$pathassign"> + <xsl:attribute name="assign"> + <xsl:value-of select="$pathassign"/> + <xsl:when test="$pathminmaxcount = 2"> + <xsl:attribute name="min"> + <xsl:value-of select="$pathminmax[1]"/> + <xsl:attribute name="max"> + <xsl:value-of select="$pathminmax[2]"/> + <xsl:when test="$pathminmaxcount = 1 or $pathminmaxcount > 2"> + <xsl:message terminate="yes"> + <xsl:text>Widget id:</xsl:text> + <xsl:value-of select="$id"/> + <xsl:text> label:</xsl:text> + <xsl:value-of select="$full_decl"/> + <xsl:text> has wrong syntax of path section </xsl:text> + <xsl:value-of select="$pathminmax"/> + <xsl:if test="$indexed_hmitree"> + <xsl:when test="regexp:test($path,'^\.[a-zA-Z0-9_]+$')"> + <xsl:attribute name="type"> + <xsl:text>PAGE_LOCAL</xsl:text> + <xsl:when test="regexp:test($path,'^[a-zA-Z0-9_]+$')"> + <xsl:attribute name="type"> + <xsl:text>HMI_LOCAL</xsl:text> + <xsl:variable name="item" select="$indexed_hmitree/*[@hmipath = $path]"/> + <xsl:variable name="pathtype" select="local-name($item)"/> + <xsl:if test="$pathminmaxcount = 3 and not($pathtype = 'HMI_INT' or $pathtype = 'HMI_REAL')"> + <xsl:message terminate="yes"> + <xsl:text>Widget id:</xsl:text> + <xsl:value-of select="$id"/> + <xsl:text> label:</xsl:text> + <xsl:value-of select="$full_decl"/> + <xsl:text> path section </xsl:text> + <xsl:value-of select="$pathminmax"/> + <xsl:text> use min and max on non mumeric value</xsl:text> + <xsl:if test="count($item) = 1"> + <xsl:attribute name="index"> + <xsl:value-of select="$item/@index"/> + <xsl:attribute name="type"> + <xsl:value-of select="$pathtype"/> + <xsl:when test="$has_continuation"> + <xsl:variable name="_continuation" select="substring-after($desc, $twonewlines)"/> + <xsl:if test="$_continuation"> + <xsl:value-of select="$_continuation"/> + <xsl:value-of select="$desc/text()"/> + <xsl:template mode="genlabel" match="arg"> + <xsl:value-of select="@value"/> + <xsl:template mode="genlabel" match="path"> + <xsl:value-of select="@value"/> + <xsl:if test="string-length(@min)>0 or string-length(@max)>0"> + <xsl:value-of select="@min"/> + <xsl:value-of select="@max"/> + <xsl:template mode="genlabel" match="widget"> + <xsl:text>HMI:</xsl:text> + <xsl:value-of select="@type"/> + <xsl:apply-templates mode="genlabel" select="arg"/> + <xsl:apply-templates mode="genlabel" select="path"/> + <xsl:variable name="hmi_elements" select="//svg:*[starts-with(@inkscape:label, 'HMI:')]"/> + <xsl:template match="widget[@type='Assign']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Arguments are either: + <xsl:text>- name=value: setting variable with literal value. + <xsl:text>- name=other_name: copy variable content into another + <xsl:text>"active"+"inactive" labeled elements can be provided to show feedback when pressed + <xsl:text>HMI:Assign:notify=1@notify=/PLCVAR + <xsl:text>HMI:Assign:ack=2:notify=1@ack=.local_var@notify=/PLCVAR + <xsl:text>Assign variables on click</xsl:text> + <xsl:template match="widget[@type='Back']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Back widget brings focus back to previous page in history when clicked. + <xsl:text>"active" + "inactive" labeled elements can be provided and reflect whether + <xsl:text>widget is pressed or not. + <xsl:text>Jump to previous page</xsl:text> + <xsl:template match="widget[@type='Button']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Button widget takes one boolean variable path, and reflect current true + <xsl:text>or false value by showing "active" or "inactive" labeled element + <xsl:text>respectively. Pressing and releasing button changes variable to true and + <xsl:text>false respectively. Potential inconsistency caused by quick consecutive + <xsl:text>presses on the button is mitigated by using a state machine that wait for + <xsl:text>previous state change to be reflected on variable before applying next one. + <xsl:text>Push button reflecting consistently given boolean variable</xsl:text> + <path name="value" accepts="HMI_BOOL"> + <xsl:text>Boolean variable</xsl:text> + <xsl:template name="generated_button_class"> + <xsl:param name="fsm"/> + <xsl:text> state = "init"; + <xsl:text> dispatch(value) { + <xsl:apply-templates mode="dispatch_transition" select="$fsm"/> + <xsl:text> onmouseup(evt) { + <xsl:text> svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); + <xsl:apply-templates mode="mouse_transition" select="$fsm"> + <xsl:with-param name="position" select="'up'"/> + <xsl:text> onmousedown(evt) { + <xsl:text> svg_root.addEventListener("pointerup", this.bound_onmouseup, true); + <xsl:apply-templates mode="mouse_transition" select="$fsm"> + <xsl:with-param name="position" select="'down'"/> + <xsl:apply-templates mode="actions" select="$fsm"/> + <xsl:text> this.bound_onmouseup = this.onmouseup.bind(this); + <xsl:text> this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); + <xsl:text> this.activity_state = undefined; + <xsl:template match="widget[@type='CircularBar']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>CircularBar widget changes the end angle of a "path" labeled arc according + <xsl:text>to value of the single accepted variable. + <xsl:text>If "min" a "max" labeled texts are provided, then they are used as + <xsl:text>respective minimum and maximum value. Otherwise, value is expected to be + <xsl:text>in between 0 and 100. + <xsl:text>Change end angle of Inkscape's arc</xsl:text> + <arg name="min" count="optional" accepts="int,real"> + <xsl:text>minimum value</xsl:text> + <arg name="max" count="optional" accepts="int,real"> + <xsl:text>maximum value</xsl:text> + <path name="value" accepts="HMI_INT,HMI_REAL"> + <xsl:text>Value to display</xsl:text> + <xsl:template match="widget[@type='CustomHtml']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>CustomHtml widget allows insertion of HTML code in a svg:foreignObject. + <xsl:text>Widget content is replaced by foreignObject. HTML code is obtained from + <xsl:text>"code" labeled text content. HTML insert position and size is given with + <xsl:text>"container" labeled element. + <xsl:text>Custom HTML insert</xsl:text> + <xsl:template match="widget[@type='Display']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>If Display widget is a svg:text element, then text content is replaced by + <xsl:text>value of given variables, space separated. + <xsl:text>Otherwise, if Display widget is a group containing a svg:text element + <xsl:text>labelled "format", then text content is replaced by printf-like formated + <xsl:text>string. In other words, if "format" labeled text is "%d %s %f", then 3 + <xsl:text>variables paths are expected : HMI_IN, HMI_STRING and HMI_REAL. + <xsl:text>In case Display widget is a svg::text element, it is also possible to give + <xsl:text>format string as first argument. + <xsl:text>Printf-like formated text display</xsl:text> + <arg name="format" count="optional" accepts="string"> + <xsl:text>printf-like format string when not given as svg:text</xsl:text> + <path name="fields" count="many" accepts="HMI_INT,HMI_REAL,HMI_STRING,HMI_BOOL"> + <xsl:text>variables to be displayed</xsl:text> + <xsl:template match="widget[@type='DropDown']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>DropDown widget can have one, two or three path variables. + <xsl:text>It needs "text" (svg:text or svg:use referring to svg:text), + <xsl:text>"box" (svg:rect), "button" (svg:*), and "highlight" (svg:rect) + <xsl:text>labeled elements. + <xsl:text>When user clicks on "button", "text" is duplicated to display entries in the + <xsl:text>limit of available space in page, and "box" is extended to contain all + <xsl:text>texts. "highlight" is moved over pre-selected entry. + <xsl:text>The first variable path is index of selection, and the second is value of selection. + <xsl:text>In case there are one or two path variables, a list of texts is defined via + <xsl:text>If there are no arguments, it is expected that "text" labeled element is of + <xsl:text>type svg:use and refers to a svg:text element part of a TextList widget. + <xsl:text>In that case list of texts is set to TextList content. + <xsl:text>When only one argument is given and its value is "#langs" then list of + <xsl:text>texts is automatically set to the human-readable list of supported + <xsl:text>languages by this HMI. + <xsl:text>Otherwise, arguments are used as dropdown options. + <xsl:text>In case there are three path variables, arguments are not expected and ignored. + <xsl:text>The third path variable is a string containing the list of entries. + <xsl:text>HMI:DropDown:Red:Green:Blue:Other@/SELECTED_INDEX@/SELECTED_VALUE + <xsl:text>HMI:DropDown@/SELECTED_INDEX@/SELECTED_VALUE@/OPTIONS + <xsl:text>Let user select text entry in a drop-down menu</xsl:text> + <arg name="entries" count="many" accepts="string"> + <xsl:text>drop-down menu entries</xsl:text> + <path name="selected_inex" accepts="HMI_INT"> + <xsl:text>selection index</xsl:text> + <path name="selected_value" accepts="HMI_STRING"> + <xsl:text>selection value</xsl:text> + <path name="options" accepts="HMI_STRING"> + <xsl:text>drop-down menu entries</xsl:text> + <xsl:template match="widget[@type='ForEach']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>ForEach widget is used to span a small set of widget over a larger set of + <xsl:text>repeated HMI_NODEs. + <xsl:text>Idea is somewhat similar to relative page, but it all happens inside the + <xsl:text>ForEach widget, no page involved. + <xsl:text>Together with relative Jump widgets it can be used to build a menu to reach + <xsl:text>relative pages covering many identical HMI_NODES siblings. + <xsl:text>ForEach widget takes a HMI_CLASS name as argument and a HMI_NODE path as + <xsl:text>Direct sub-elements can be either groups of widget to be spanned, labeled + <xsl:text>"ClassName:offset", or buttons to control the spanning, labeled + <xsl:text>"ClassName:+/-number". + <xsl:text>In case of "ClassName:offset", offset for first element is 1. + <xsl:text>span widgets over a set of repeated HMI_NODEs</xsl:text> + <arg name="class_name" accepts="string"> + <xsl:text>HMI_CLASS name</xsl:text> + <path name="root" accepts="HMI_NODE"> + <xsl:text> where to find HMI_NODEs whose HMI_CLASS is class_name</xsl:text> + <path name="position" accepts="HMI_INT"> + <xsl:text>position of HMI_NODE mapped to first item, among similar siblings</xsl:text> + <path name="range" accepts="HMI_INT" count="optional"> + <xsl:text> count of HMI_NODE siblings</xsl:text> + <path name="size" accepts="HMI_INT" count="optional"> + <xsl:text> count of visible items</xsl:text> + <xsl:template match="widget[@type='Image']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>If Image widget is a svg:image element, then href content is replaced by + <xsl:text>value of given variable. + <xsl:text>Image display</xsl:text> + <xsl:template match="widget[@type='Input']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Input widget takes one variable path, and displays current value in + <xsl:text>optional "value" labeled sub-element. + <xsl:text>Click on optional "edit" labeled element opens keypad to edit value. + <xsl:text>Operation on current value is performed when click on sub-elements with + <xsl:text>label starting with '=', '+' or '-' sign. Value after sign is used as + <xsl:text>Input field with predefined operation buttons</xsl:text> + <arg name="format" accepts="string"> + <xsl:text>optional printf-like format </xsl:text> + <path name="edit" accepts="HMI_INT, HMI_REAL, HMI_STRING"> + <xsl:text>single variable to edit</xsl:text> + <xsl:template match="widget[@type='JsonTable']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Send given variables as POST to http URL argument, spread returned JSON in + <xsl:text>SVG sub-elements of "data" labeled element. + <xsl:text>Documentation to be written. see svghmi example. + <xsl:text>Http POST variables, spread JSON back</xsl:text> + <arg name="url" accepts="string"> + <path name="edit" accepts="HMI_INT, HMI_REAL, HMI_STRING"> + <xsl:text>single variable to edit</xsl:text> + <xsl:template match="widget[@type='Jump']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Jump widget brings focus to a different page. Mandatory first argument + <xsl:text>gives name of the page. + <xsl:text>If first path is pointing to HMI_NODE variable is used as new reference + <xsl:text>when jumping to a relative page. + <xsl:text>Additional arguments are unordered options: + <xsl:text>- Absolute: force page jump to be not relative even if first path is of type HMI_NODE + <xsl:text>- name=value: Notify PLC about jump by setting variable with path having same name assigned + <xsl:text>"active"+"inactive" labeled elements can be provided and reflect current + <xsl:text>page being shown. + <xsl:text>Relative jump: + <xsl:text>HMI:Jump:RelativePage@/PUMP9 + <xsl:text>HMI:Jump:RelativePage@/PUMP9@role=.userrole#role=="admin" + <xsl:text>Absolute jump: + <xsl:text>HMI:Jump:AbsolutePage + <xsl:text>HMI:Jump:AbsolutePage@role=.userrole#role=="admin" + <xsl:text>Forced absolute jump: + <xsl:text>HMI:Jump:AbsolutePage:Absolute@/PUMP9 + <xsl:text>HMI:Jump:AbsolutePage:Absolute:notify=1@notify=/PUMP9 + <xsl:text>Jump with feedback + <xsl:text>HMI:Jump:AbsolutePage:notify=1@notify=.did_jump + <xsl:text>Jump to given page</xsl:text> + <arg name="page" accepts="string"> + <xsl:text>name of page to jump to</xsl:text> + <path name="reference" count="optional" accepts="HMI_NODE"> + <xsl:text>reference for relative jump</xsl:text> + <func:function name="func:is_relative_jump"> + <xsl:param name="widget"/> + <func:result select="$widget/path and $widget/path[1]/@type='HMI_NODE' and not($widget/arg[position()>1 and @value = 'Absolute'])"/> + <xsl:template match="widget[@type='Keypad']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Keypad - to be written + <xsl:text>Keypad </xsl:text> + <arg name="supported_types" accepts="string"> + <xsl:text>keypad can input those types </xsl:text> + <xsl:template match="widget[@type='List']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>List widget is a svg:group, list items are labeled elements + <xsl:text>in that group. + <xsl:text>To use a List, clone (svg:use) one of the items inside the widget that + <xsl:text>expects a List. + <xsl:text>Positions of items are relative to each other, and they must all be in the + <xsl:text>same place. In order to make editing easier it is therefore recommanded to + <xsl:text>make stacked clones of svg elements spread nearby the list. + <xsl:text>A named list of named graphical elements</xsl:text> + <xsl:template match="widget[@type='ListSwitch']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>ListSwitch widget displays one item of an HMI:List depending on value of + <xsl:text>given variable. Main element of the widget must be a clone of the list or + <xsl:text>of an item of that list. + <xsl:text>Given variable's current value is compared to list items + <xsl:text>label. For exemple if given variable type + <xsl:text>is HMI_INT and value is 1, then item with label '1' will be displayed. + <xsl:text>If matching variable of type HMI_STRING, then no quotes are needed. + <xsl:text>For exemple, 'hello' match HMI_STRING 'hello'. + <xsl:text>Displays item of an HMI:List whose label matches value.</xsl:text> + <path name="value" accepts="HMI_INT,HMI_STRING"> + <xsl:text>value to compare to labels</xsl:text> + <xsl:template match="widget[@type='Meter']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Meter widget moves the end of "needle" labeled path along "range" labeled + <xsl:text>path, according to value of the single accepted variable. + <xsl:text>Needle is reduced to a single segment. If "min" a "max" labeled texts + <xsl:text>are provided, or if first and second argument are given, then they are used + <xsl:text>as respective minimum and maximum value. Otherwise, value is expected to be + <xsl:text>in between 0 and 100. + <xsl:text>Moves "needle" along "range"</xsl:text> + <arg name="min" count="optional" accepts="int,real"> + <xsl:text>minimum value</xsl:text> + <arg name="max" count="optional" accepts="int,real"> + <xsl:text>maximum value</xsl:text> + <path name="value" accepts="HMI_INT,HMI_REAL"> + <xsl:text>Value to display</xsl:text> + <xsl:template match="widget[@type='Page']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Arguments are either: + <xsl:text>- XXX reference path TODO + <xsl:text>- name=value: setting variable with literal value. + <xsl:text>- name=other_name: copy variable content into another + <xsl:text>HMI:Page:notify=1@notify=/PLCVAR + <xsl:text>HMI:Page:ack=2:notify=1@ack=.local_var@notify=/PLCVAR + <xsl:text>Page </xsl:text> + <xsl:template match="widget[@type='PathSlider']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Slide an SVG element along a path by dragging it</xsl:text> + <path name="value" accepts="HMI_INT,HMI_REAL"> + <xsl:text>value</xsl:text> + <path name="min" count="optional" accepts="HMI_INT,HMI_REAL"> + <xsl:text>min</xsl:text> + <path name="max" count="optional" accepts="HMI_INT,HMI_REAL"> + <xsl:text>max</xsl:text> + <arg name="min" count="optional" accepts="int,real"> + <xsl:text>minimum value</xsl:text> + <arg name="max" count="optional" accepts="int,real"> + <xsl:text>maximum value</xsl:text> + <xsl:template match="widget[@type='ScrollBar']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>ScrollBar - svg:rect based scrollbar + <xsl:text>ScrollBar</xsl:text> + <path name="value" accepts="HMI_INT"> + <xsl:text>value</xsl:text> + <path name="range" accepts="HMI_INT"> + <xsl:text>range</xsl:text> + <path name="visible" accepts="HMI_INT"> + <xsl:text>visible</xsl:text> + <xsl:template match="widget[@type='Switch']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Switch widget hides all subelements whose label do not match given + <xsl:text>variable current value representation. For exemple if given variable type + <xsl:text>is HMI_INT and value is 1, then elements with label '1' will be displayed. + <xsl:text>Label can have comments, so '1#some comment' would also match. If matching + <xsl:text>variable of type HMI_STRING, then double quotes must be used. For exemple, + <xsl:text>'"hello"' or '"hello"#another comment' match HMI_STRING 'hello'. + <xsl:text>Show elements whose label matches value.</xsl:text> + <path name="value" accepts="HMI_INT,HMI_STRING"> + <xsl:text>value to compare to labels</xsl:text> + <xsl:template match="widget[@type='TextList']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>TextList widget is a svg:group, list items are labeled elements + <xsl:text>in that group. + <xsl:text>To use a TextList, clone (svg:use) one of the items inside the widget + <xsl:text>that expects a TextList. + <xsl:text>In this list, (translated) text content is what matters. Nevertheless + <xsl:text>text style of the cloned item will be applied in client widget. + <xsl:text>A named list of ordered texts </xsl:text> + <xsl:template match="widget[@type='TextStyleList']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>TextStyleList widget is a svg:group, list items are labeled elements + <xsl:text>in that group. + <xsl:text>To use a TextStyleList, clone (svg:use) one of the items inside the widget + <xsl:text>that expects a TextStyleList. + <xsl:text>In this list, only style matters. Text content is ignored. + <xsl:text>A named list of named texts</xsl:text> + <xsl:template match="widget[@type='ToggleButton']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Button widget takes one boolean variable path, and reflect current true + <xsl:text>or false value by showing "active" or "inactive" labeled element + <xsl:text>respectively. Clicking or touching button toggles variable. + <xsl:text>Toggle button reflecting given boolean variable</xsl:text> + <path name="value" accepts="HMI_BOOL"> + <xsl:text>Boolean variable</xsl:text> + <xsl:template match="widget[@type='XYGraph']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>XYGraph draws a cartesian trend graph re-using styles given for axis, + <xsl:text>grid/marks, legends and curves. + <xsl:text>Elements labeled "x_axis" and "y_axis" are svg:groups containg: + <xsl:text> - "axis_label" svg:text gives style an alignment for axis labels. + <xsl:text> - "interval_major_mark" and "interval_minor_mark" are svg elements to be + <xsl:text> duplicated along axis line to form intervals marks. + <xsl:text> - "axis_line" svg:path is the axis line. Paths must be intersect and their + <xsl:text> bounding box is the chart wall. + <xsl:text>Elements labeled "curve_0", "curve_1", ... are paths whose styles are used + <xsl:text>to draw curves corresponding to data from variables passed as HMI tree paths. + <xsl:text>"curve_0" is mandatory. HMI variables outnumbering given curves are ignored. + <xsl:text>Cartesian trend graph showing values of given variables over time</xsl:text> + <path name="value" count="1+" accepts="HMI_INT,HMI_REAL"> + <xsl:text>value</xsl:text> + <arg name="xrange" accepts="int,time"> + <xsl:text>X axis range expressed either in samples or duration.</xsl:text> + <arg name="xformat" count="optional" accepts="string"> + <xsl:text>format string for X label</xsl:text> + <arg name="yformat" count="optional" accepts="string"> + <xsl:text>format string for Y label</xsl:text> + <func:function name="func:check_curves_label_consistency"> + <xsl:param name="curve_elts"/> + <xsl:param name="number_to_check"/> + <xsl:variable name="res"> + <xsl:when test="$curve_elts[@inkscape:label = concat('curve_', string($number_to_check))]"> + <xsl:if test="$number_to_check > 0"> + <xsl:value-of select="func:check_curves_label_consistency($curve_elts, $number_to_check - 1)"/> + <xsl:value-of select="concat('missing curve_', string($number_to_check))"/> + <func:result select="$res"/> + <xsl:template match="widget[@type='Swipe']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Swipe widget detects left, right, up and down swiping motion and executes + <xsl:text>associated actions. + <xsl:text>For each of the motions to be detected there must exist a group inside the + <xsl:text>widget with the label "dir_{direction}" where {direction} is from the set: + <xsl:text>left, right, up, down. + <xsl:text>Inside each such group, there should be an element with the label + <xsl:text>"thresholds:{x},{y}", where {x} and {y} represent pixel count along X and Y + <xsl:text>axis that define the motion needed to be labelled as swipe. + <xsl:text>Alongside that, one can add up to one element with the label + <xsl:text>"jump:{pagename}", where {pagename} is the name of the page which is the + <xsl:text>target of a jump. + <xsl:text>There can also be any number of elements with the label + <xsl:text>"{expression}:{variable}", where {expression} is a mathematical expression + <xsl:text>to be applied to {variable}. All variables that appear in such elements + <xsl:text>must also be listed as paths in the widget label. + <xsl:text>HMI:Swipe@/VAR0@/VAR1 + <xsl:text> - thresholds:-100,20 + <xsl:text> - thresholds:20,100 + <xsl:text>This detects left and up swipe motion. To detect swipe left, movement must + <xsl:text>be at least 100 pixels to the left and at most 20 pixels up or down. If + <xsl:text>detected, it will increase VAR0 by 1 and jump to a page named Home. To + <xsl:text>detect swipe up, movement must be at most 20 pixels left or right, and at + <xsl:text>least 100 pixels up. If detected, it will decrease VAR0 by 1 and set VAR1 + <xsl:text>Detect swipe motion and react accordingly</xsl:text> + <path name="variable" count="optional" accepts="all"/> + <xsl:template mode="document" match="@* | node()"> + <xsl:apply-templates mode="document" select="@* | node()"/> + <xsl:template mode="document" match="widget"> + <xsl:apply-templates mode="document" select="@* | node()"/> + <xsl:apply-templates mode="widget_desc" select="."/> + <xsl:template match="/"> + <xsl:variable name="widgets"> + <xsl:apply-templates mode="parselabel" select="$hmi_elements"/> + <xsl:variable name="widget_ns" select="exsl:node-set($widgets)"/> + <xsl:apply-templates mode="document" select="$widget_ns"/> --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/LPCSVGHMI/gen_dnd_widget_svg.xslt Thu May 29 15:56:09 2025 +0200
@@ -0,0 +1,309 @@
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="http://exslt.org/common" xmlns:regexp="http://exslt.org/regular-expressions" xmlns:str="http://exslt.org/strings" xmlns:func="http://exslt.org/functions" 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:ns="beremiz" version="1.0" extension-element-prefixes="ns func exsl regexp str dyn" exclude-result-prefixes="ns func exsl regexp str dyn"> + <xsl:output method="xml"/> + <xsl:variable name="hmi_elements" select="//svg:*[starts-with(@inkscape:label, 'HMI:')]"/> + <xsl:variable name="widgetparams" select="ns:GetWidgetParams()"/> + <xsl:variable name="indexed_hmitree" select="/.."/> + <xsl:variable name="pathregex" select="'^(\w+=)?([^,=]+)([-.\w,]*)$'"/> + <xsl:variable name="newline"> + <xsl:variable name="twonewlines" select="concat($newline,$newline)"/> + <xsl:template mode="parselabel" match="*"> + <xsl:variable name="label" select="@inkscape:label"/> + <xsl:variable name="desc" select="svg:desc"/> + <xsl:variable name="len" select="string-length($label)"/> + <xsl:variable name="has_continuation" select="substring($label,$len,1)='\'"/> + <xsl:variable name="full_decl"> + <xsl:when test="$has_continuation"> + <xsl:variable name="_continuation" select="substring-before($desc, $twonewlines)"/> + <xsl:variable name="continuation"> + <xsl:when test="$_continuation"> + <xsl:value-of select="$_continuation"/> + <xsl:value-of select="$desc"/> + <xsl:value-of select="concat(substring($label,1,$len - 1),translate($continuation,$newline,''))"/> + <xsl:value-of select="$label"/> + <xsl:variable name="id" select="@id"/> + <xsl:variable name="declaration" select="substring-after($full_decl,'HMI:')"/> + <xsl:variable name="_args" select="substring-before($declaration,'@')"/> + <xsl:variable name="args"> + <xsl:when test="$_args"> + <xsl:value-of select="$_args"/> + <xsl:value-of select="$declaration"/> + <xsl:variable name="_typefreq" select="substring-before($args,':')"/> + <xsl:variable name="typefreq"> + <xsl:when test="$_typefreq"> + <xsl:value-of select="$_typefreq"/> + <xsl:value-of select="$args"/> + <xsl:variable name="freq" select="substring-after($typefreq,'|')"/> + <xsl:variable name="_type" select="substring-before($typefreq,'|')"/> + <xsl:variable name="type"> + <xsl:when test="$_type"> + <xsl:value-of select="$_type"/> + <xsl:value-of select="$typefreq"/> + <xsl:attribute name="id"> + <xsl:value-of select="$id"/> + <xsl:attribute name="type"> + <xsl:value-of select="$type"/> + <xsl:if test="not(regexp:test($freq,'^[0-9]*(\.[0-9]+)?[smh]?'))"> + <xsl:message terminate="yes"> + <xsl:text>Widget id:</xsl:text> + <xsl:value-of select="$id"/> + <xsl:text> label:</xsl:text> + <xsl:value-of select="$full_decl"/> + <xsl:text> has wrong syntax of frequency forcing </xsl:text> + <xsl:value-of select="$freq"/> + <xsl:attribute name="freq"> + <xsl:value-of select="$freq"/> + <xsl:variable name="tail" select="substring-after($declaration,'@')"/> + <xsl:variable name="taillen" select="string-length($tail)"/> + <xsl:variable name="has_enable" select="contains($tail, '#')"/> + <xsl:variable name="paths"> + <xsl:when test="$has_enable"> + <xsl:value-of select="substring-before($tail,'#')"/> + <xsl:value-of select="$tail"/> + <xsl:if test="$has_enable"> + <xsl:variable name="enable_expr" select="substring-after($tail,'#')"/> + <xsl:attribute name="enable_expr"> + <xsl:value-of select="$enable_expr"/> + <xsl:for-each select="str:split(substring-after($args, ':'), ':')"> + <xsl:attribute name="value"> + <xsl:value-of select="."/> + <xsl:for-each select="str:split($paths, '@')"> + <xsl:if test="string-length(.) > 0"> + <xsl:variable name="path_match" select="regexp:match(.,$pathregex)"/> + <xsl:variable name="pathassign" select="substring-before($path_match[2],'=')"/> + <xsl:variable name="pathminmax" select="str:split($path_match[4],',')"/> + <xsl:variable name="path" select="$path_match[3]"/> + <xsl:variable name="pathminmaxcount" select="count($pathminmax)"/> + <xsl:if test="not($path)"> + <xsl:message terminate="yes"> + <xsl:text>Widget id:</xsl:text> + <xsl:value-of select="$id"/> + <xsl:text> label:</xsl:text> + <xsl:value-of select="$full_decl"/> + <xsl:text> has wrong syntax</xsl:text> + <xsl:attribute name="value"> + <xsl:value-of select="$path"/> + <xsl:if test="$pathassign"> + <xsl:attribute name="assign"> + <xsl:value-of select="$pathassign"/> + <xsl:when test="$pathminmaxcount = 2"> + <xsl:attribute name="min"> + <xsl:value-of select="$pathminmax[1]"/> + <xsl:attribute name="max"> + <xsl:value-of select="$pathminmax[2]"/> + <xsl:when test="$pathminmaxcount = 1 or $pathminmaxcount > 2"> + <xsl:message terminate="yes"> + <xsl:text>Widget id:</xsl:text> + <xsl:value-of select="$id"/> + <xsl:text> label:</xsl:text> + <xsl:value-of select="$full_decl"/> + <xsl:text> has wrong syntax of path section </xsl:text> + <xsl:value-of select="$pathminmax"/> + <xsl:if test="$indexed_hmitree"> + <xsl:when test="regexp:test($path,'^\.[a-zA-Z0-9_]+$')"> + <xsl:attribute name="type"> + <xsl:text>PAGE_LOCAL</xsl:text> + <xsl:when test="regexp:test($path,'^[a-zA-Z0-9_]+$')"> + <xsl:attribute name="type"> + <xsl:text>HMI_LOCAL</xsl:text> + <xsl:variable name="item" select="$indexed_hmitree/*[@hmipath = $path]"/> + <xsl:variable name="pathtype" select="local-name($item)"/> + <xsl:if test="$pathminmaxcount = 3 and not($pathtype = 'HMI_INT' or $pathtype = 'HMI_REAL')"> + <xsl:message terminate="yes"> + <xsl:text>Widget id:</xsl:text> + <xsl:value-of select="$id"/> + <xsl:text> label:</xsl:text> + <xsl:value-of select="$full_decl"/> + <xsl:text> path section </xsl:text> + <xsl:value-of select="$pathminmax"/> + <xsl:text> use min and max on non mumeric value</xsl:text> + <xsl:if test="count($item) = 1"> + <xsl:attribute name="index"> + <xsl:value-of select="$item/@index"/> + <xsl:attribute name="type"> + <xsl:value-of select="$pathtype"/> + <xsl:when test="$has_continuation"> + <xsl:variable name="_continuation" select="substring-after($desc, $twonewlines)"/> + <xsl:if test="$_continuation"> + <xsl:value-of select="$_continuation"/> + <xsl:value-of select="$desc/text()"/> + <xsl:template mode="genlabel" match="arg"> + <xsl:value-of select="@value"/> + <xsl:template mode="genlabel" match="path"> + <xsl:value-of select="@value"/> + <xsl:if test="string-length(@min)>0 or string-length(@max)>0"> + <xsl:value-of select="@min"/> + <xsl:value-of select="@max"/> + <xsl:template mode="genlabel" match="widget"> + <xsl:text>HMI:</xsl:text> + <xsl:value-of select="@type"/> + <xsl:apply-templates mode="genlabel" select="arg"/> + <xsl:apply-templates mode="genlabel" select="path"/> + <xsl:variable name="_parsed_widgets"> + <xsl:apply-templates mode="parselabel" select="$hmi_elements"/> + <xsl:variable name="parsed_widgets" select="exsl:node-set($_parsed_widgets)"/> + <xsl:variable name="svg_widget" select="$parsed_widgets/widget[1]"/> + <xsl:variable name="svg_widget_type" select="$svg_widget/@type"/> + <xsl:variable name="svg_widget_path" select="$svg_widget/@path"/> + <xsl:variable name="svg_widget_count" select="count($parsed_widgets/widget)"/> + <xsl:template mode="replace_params" match="@* | node()"> + <xsl:apply-templates mode="replace_params" select="@* | node()"/> + <xsl:template mode="replace_params" match="arg"/> + <xsl:template mode="replace_params" match="path"/> + <xsl:template mode="replace_params" match="widget"> + <xsl:apply-templates mode="replace_params" select="@* | node()"/> + <xsl:copy-of select="$widgetparams/*"/> + <xsl:template xmlns="http://www.w3.org/2000/svg" mode="inline_svg" match="@*"> + <xsl:template xmlns="http://www.w3.org/2000/svg" mode="inline_svg" match="@inkscape:label[starts-with(., 'HMI:')]"/> + <xsl:template mode="inline_svg" match="node()"> + <xsl:if test="@id = $svg_widget/@id"> + <xsl:variable name="substituted_widget"> + <xsl:apply-templates mode="replace_params" select="$svg_widget"/> + <xsl:variable name="substituted_widget_ns" select="exsl:node-set($substituted_widget)"/> + <xsl:variable name="new_label"> + <xsl:apply-templates mode="genlabel" select="$substituted_widget_ns"/> + <xsl:attribute name="inkscape:label"> + <xsl:value-of select="$new_label"/> + <xsl:apply-templates mode="inline_svg" select="@* | node()"/> + <xsl:template match="/"> + <xsl:text>Widget dropped in Inkscape from Beremiz</xsl:text> + <xsl:when test="$svg_widget_count < 1"> + <xsl:message terminate="yes"> + <xsl:text>No widget detected on selected SVG</xsl:text> + <xsl:when test="$svg_widget_count > 1"> + <xsl:message terminate="yes"> + <xsl:text>Multiple widget DnD not yet supported</xsl:text> + <xsl:apply-templates mode="inline_svg" select="/"/> --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/LPCSVGHMI/gen_index_xhtml.xslt Thu May 29 15:56:09 2025 +0200
@@ -0,0 +1,12218 @@
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="http://exslt.org/common" xmlns:regexp="http://exslt.org/regular-expressions" xmlns:str="http://exslt.org/strings" xmlns:func="http://exslt.org/functions" 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" xmlns:debug="debug" xmlns:preamble="preamble" xmlns:declarations="declarations" xmlns:definitions="definitions" xmlns:epilogue="epilogue" xmlns:cssdefs="cssdefs" xmlns:ns="beremiz" version="1.0" extension-element-prefixes="ns func exsl regexp str dyn" exclude-result-prefixes="ns func exsl regexp str dyn debug preamble epilogue declarations definitions"> + <xsl:output cdata-section-elements="xhtml:script" method="xml"/> + <xsl:variable name="svg" select="/svg:svg"/> + <xsl:variable name="hmi_elements" select="//svg:*[starts-with(@inkscape:label, 'HMI:')]"/> + <xsl:param name="instance_name"/> + <xsl:variable name="hmitree" select="ns:GetHMITree()"/> + <xsl:variable name="_indexed_hmitree"> + <xsl:apply-templates mode="index" select="$hmitree"/> + <xsl:variable name="indexed_hmitree" select="exsl:node-set($_indexed_hmitree)"/> + <xsl:template match="preamble:hmi-tree"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:text>var hmi_hash = [</xsl:text> + <xsl:value-of select="$hmitree/@hash"/> + <xsl:text>var heartbeat_index = </xsl:text> + <xsl:value-of select="$indexed_hmitree/*[@hmipath = '/HEARTBEAT']/@index"/> + <xsl:text>var current_page_var_index = </xsl:text> + <xsl:value-of select="$indexed_hmitree/*[@hmipath = concat('/CURRENT_PAGE_', $instance_name)]/@index"/> + <xsl:text>var hmitree_types = [ + <xsl:for-each select="$indexed_hmitree/*"> + <xsl:text> "</xsl:text> + <xsl:value-of select="substring(local-name(), 5)"/> + <xsl:if test="position()!=last()"> + <xsl:text>var hmitree_paths = [ + <xsl:for-each select="$indexed_hmitree/*"> + <xsl:text> "</xsl:text> + <xsl:value-of select="@hmipath"/> + <xsl:if test="position()!=last()"> + <xsl:text>var hmitree_nodes = { + <xsl:for-each select="$indexed_hmitree/*[local-name() = 'HMI_NODE']"> + <xsl:text> "</xsl:text> + <xsl:value-of select="@hmipath"/> + <xsl:text>" : [</xsl:text> + <xsl:value-of select="@index"/> + <xsl:text>, "</xsl:text> + <xsl:value-of select="@class"/> + <xsl:text>"]</xsl:text> + <xsl:if test="position()!=last()"> + <xsl:template mode="index" match="*"> + <xsl:param name="index" select="0"/> + <xsl:param name="parentpath" select="''"/> + <xsl:variable name="content"> + <xsl:variable name="path"> + <xsl:when test="count(ancestor::*)=0"> + <xsl:when test="count(ancestor::*)=1"> + <xsl:value-of select="@name"/> + <xsl:value-of select="$parentpath"/> + <xsl:value-of select="@name"/> + <xsl:attribute name="index"> + <xsl:value-of select="$index"/> + <xsl:attribute name="hmipath"> + <xsl:value-of select="$path"/> + <xsl:for-each select="@*"> + <xsl:apply-templates mode="index" select="*[1]"> + <xsl:with-param name="index" select="$index + 1"/> + <xsl:with-param name="parentpath"> + <xsl:value-of select="$path"/> + <xsl:copy-of select="$content"/> + <xsl:apply-templates mode="index" select="following-sibling::*[1]"> + <xsl:with-param name="index" select="$index + count(exsl:node-set($content)/*)"/> + <xsl:with-param name="parentpath"> + <xsl:value-of select="$parentpath"/> + <xsl:variable name="pathregex" select="'^(\w+=)?([^,=]+)([-.\w,]*)$'"/> + <xsl:variable name="newline"> + <xsl:variable name="twonewlines" select="concat($newline,$newline)"/> + <xsl:template mode="parselabel" match="*"> + <xsl:variable name="label" select="@inkscape:label"/> + <xsl:variable name="desc" select="svg:desc"/> + <xsl:variable name="len" select="string-length($label)"/> + <xsl:variable name="has_continuation" select="substring($label,$len,1)='\'"/> + <xsl:variable name="full_decl"> + <xsl:when test="$has_continuation"> + <xsl:variable name="_continuation" select="substring-before($desc, $twonewlines)"/> + <xsl:variable name="continuation"> + <xsl:when test="$_continuation"> + <xsl:value-of select="$_continuation"/> + <xsl:value-of select="$desc"/> + <xsl:value-of select="concat(substring($label,1,$len - 1),translate($continuation,$newline,''))"/> + <xsl:value-of select="$label"/> + <xsl:variable name="id" select="@id"/> + <xsl:variable name="declaration" select="substring-after($full_decl,'HMI:')"/> + <xsl:variable name="_args" select="substring-before($declaration,'@')"/> + <xsl:variable name="args"> + <xsl:when test="$_args"> + <xsl:value-of select="$_args"/> + <xsl:value-of select="$declaration"/> + <xsl:variable name="_typefreq" select="substring-before($args,':')"/> + <xsl:variable name="typefreq"> + <xsl:when test="$_typefreq"> + <xsl:value-of select="$_typefreq"/> + <xsl:value-of select="$args"/> + <xsl:variable name="freq" select="substring-after($typefreq,'|')"/> + <xsl:variable name="_type" select="substring-before($typefreq,'|')"/> + <xsl:variable name="type"> + <xsl:when test="$_type"> + <xsl:value-of select="$_type"/> + <xsl:value-of select="$typefreq"/> + <xsl:attribute name="id"> + <xsl:value-of select="$id"/> + <xsl:attribute name="type"> + <xsl:value-of select="$type"/> + <xsl:if test="not(regexp:test($freq,'^[0-9]*(\.[0-9]+)?[smh]?'))"> + <xsl:message terminate="yes"> + <xsl:text>Widget id:</xsl:text> + <xsl:value-of select="$id"/> + <xsl:text> label:</xsl:text> + <xsl:value-of select="$full_decl"/> + <xsl:text> has wrong syntax of frequency forcing </xsl:text> + <xsl:value-of select="$freq"/> + <xsl:attribute name="freq"> + <xsl:value-of select="$freq"/> + <xsl:variable name="tail" select="substring-after($declaration,'@')"/> + <xsl:variable name="taillen" select="string-length($tail)"/> + <xsl:variable name="has_enable" select="contains($tail, '#')"/> + <xsl:variable name="paths"> + <xsl:when test="$has_enable"> + <xsl:value-of select="substring-before($tail,'#')"/> + <xsl:value-of select="$tail"/> + <xsl:if test="$has_enable"> + <xsl:variable name="enable_expr" select="substring-after($tail,'#')"/> + <xsl:attribute name="enable_expr"> + <xsl:value-of select="$enable_expr"/> + <xsl:for-each select="str:split(substring-after($args, ':'), ':')"> + <xsl:attribute name="value"> + <xsl:value-of select="."/> + <xsl:for-each select="str:split($paths, '@')"> + <xsl:if test="string-length(.) > 0"> + <xsl:variable name="path_match" select="regexp:match(.,$pathregex)"/> + <xsl:variable name="pathassign" select="substring-before($path_match[2],'=')"/> + <xsl:variable name="pathminmax" select="str:split($path_match[4],',')"/> + <xsl:variable name="path" select="$path_match[3]"/> + <xsl:variable name="pathminmaxcount" select="count($pathminmax)"/> + <xsl:if test="not($path)"> + <xsl:message terminate="yes"> + <xsl:text>Widget id:</xsl:text> + <xsl:value-of select="$id"/> + <xsl:text> label:</xsl:text> + <xsl:value-of select="$full_decl"/> + <xsl:text> has wrong syntax</xsl:text> + <xsl:attribute name="value"> + <xsl:value-of select="$path"/> + <xsl:if test="$pathassign"> + <xsl:attribute name="assign"> + <xsl:value-of select="$pathassign"/> + <xsl:when test="$pathminmaxcount = 2"> + <xsl:attribute name="min"> + <xsl:value-of select="$pathminmax[1]"/> + <xsl:attribute name="max"> + <xsl:value-of select="$pathminmax[2]"/> + <xsl:when test="$pathminmaxcount = 1 or $pathminmaxcount > 2"> + <xsl:message terminate="yes"> + <xsl:text>Widget id:</xsl:text> + <xsl:value-of select="$id"/> + <xsl:text> label:</xsl:text> + <xsl:value-of select="$full_decl"/> + <xsl:text> has wrong syntax of path section </xsl:text> + <xsl:value-of select="$pathminmax"/> + <xsl:if test="$indexed_hmitree"> + <xsl:when test="regexp:test($path,'^\.[a-zA-Z0-9_]+$')"> + <xsl:attribute name="type"> + <xsl:text>PAGE_LOCAL</xsl:text> + <xsl:when test="regexp:test($path,'^[a-zA-Z0-9_]+$')"> + <xsl:attribute name="type"> + <xsl:text>HMI_LOCAL</xsl:text> + <xsl:variable name="item" select="$indexed_hmitree/*[@hmipath = $path]"/> + <xsl:variable name="pathtype" select="local-name($item)"/> + <xsl:if test="$pathminmaxcount = 3 and not($pathtype = 'HMI_INT' or $pathtype = 'HMI_REAL')"> + <xsl:message terminate="yes"> + <xsl:text>Widget id:</xsl:text> + <xsl:value-of select="$id"/> + <xsl:text> label:</xsl:text> + <xsl:value-of select="$full_decl"/> + <xsl:text> path section </xsl:text> + <xsl:value-of select="$pathminmax"/> + <xsl:text> use min and max on non mumeric value</xsl:text> + <xsl:if test="count($item) = 1"> + <xsl:attribute name="index"> + <xsl:value-of select="$item/@index"/> + <xsl:attribute name="type"> + <xsl:value-of select="$pathtype"/> + <xsl:when test="$has_continuation"> + <xsl:variable name="_continuation" select="substring-after($desc, $twonewlines)"/> + <xsl:if test="$_continuation"> + <xsl:value-of select="$_continuation"/> + <xsl:value-of select="$desc/text()"/> + <xsl:template mode="genlabel" match="arg"> + <xsl:value-of select="@value"/> + <xsl:template mode="genlabel" match="path"> + <xsl:value-of select="@value"/> + <xsl:if test="string-length(@min)>0 or string-length(@max)>0"> + <xsl:value-of select="@min"/> + <xsl:value-of select="@max"/> + <xsl:template mode="genlabel" match="widget"> + <xsl:text>HMI:</xsl:text> + <xsl:value-of select="@type"/> + <xsl:apply-templates mode="genlabel" select="arg"/> + <xsl:apply-templates mode="genlabel" select="path"/> + <xsl:variable name="_parsed_widgets"> + <widget type="VarInitPersistent"> + <xsl:apply-templates mode="parselabel" select="$hmi_elements"/> + <xsl:variable name="parsed_widgets" select="exsl:node-set($_parsed_widgets)"/> + <func:function name="func:widget"> + <func:result select="$parsed_widgets/widget[@id = $id]"/> + <func:function name="func:is_descendant_path"> + <xsl:param name="descend"/> + <xsl:param name="ancest"/> + <func:result select="string-length($ancest) > 0 and starts-with($descend,$ancest)"/> + <func:function name="func:same_class_paths"> + <xsl:variable name="class_a" select="$indexed_hmitree/*[@hmipath = $a]/@class"/> + <xsl:variable name="class_b" select="$indexed_hmitree/*[@hmipath = $b]/@class"/> + <func:result select="$class_a and $class_b and $class_a = $class_b"/> + <xsl:template mode="testtree" match="*"> + <xsl:param name="indent" select="''"/> + <xsl:value-of select="$indent"/> + <xsl:value-of select="local-name()"/> + <xsl:for-each select="@*"> + <xsl:value-of select="local-name()"/> + <xsl:text>="</xsl:text> + <xsl:value-of select="."/> + <xsl:text>" </xsl:text> + <xsl:value-of select="text()"/> + <xsl:apply-templates mode="testtree" select="*"> + <xsl:with-param name="indent"> + <xsl:value-of select="concat($indent,'>')"/> + <xsl:template match="debug:hmi-tree"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:apply-templates mode="testtree" select="$hmitree"/> + <xsl:text>Indexed HMI tree + <xsl:apply-templates mode="testtree" select="$indexed_hmitree"/> + <xsl:text>Parsed Widgets + <xsl:copy-of select="_parsed_widgets"/> + <xsl:apply-templates mode="testtree" select="$parsed_widgets"/> + <xsl:variable name="all_geometry" select="ns:GetSVGGeometry()"/> + <xsl:variable name="defs" select="//svg:defs/descendant-or-self::svg:*"/> + <xsl:variable name="geometry" select="$all_geometry[not(@Id = $defs/@id)]"/> + <xsl:template match="debug:geometry"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:text>ID, x, y, w, h + <xsl:for-each select="$geometry"> + <xsl:value-of select="@Id"/> + <xsl:value-of select="@x"/> + <xsl:value-of select="@y"/> + <xsl:value-of select="@w"/> + <xsl:value-of select="@h"/> + <func:function name="func:intersect_1d"> + <xsl:variable name="d0" select="$a0 >= $b0"/> + <xsl:variable name="d1" select="$a1 >= $b1"/> + <xsl:when test="not($d0) and $d1"> + <func:result select="3"/> + <xsl:when test="$d0 and not($d1)"> + <func:result select="2"/> + <xsl:when test="$d0 and $d1 and $a0 < $b1"> + <func:result select="1"/> + <xsl:when test="not($d0) and not($d1) and $b0 < $a1"> + <func:result select="1"/> + <func:result select="0"/> + <func:function name="func:intersect"> + <xsl:variable name="x_intersect" select="func:intersect_1d($a/@x, $a/@x+$a/@w, $b/@x, $b/@x+$b/@w)"/> + <xsl:when test="$x_intersect != 0"> + <xsl:variable name="y_intersect" select="func:intersect_1d($a/@y, $a/@y+$a/@h, $b/@y, $b/@y+$b/@h)"/> + <func:result select="$x_intersect * $y_intersect"/> + <func:result select="0"/> + <xsl:variable name="groups" select="/svg:svg | //svg:g"/> + <func:function name="func:overlapping_geometry"> + <xsl:param name="elt"/> + <xsl:variable name="g" select="$geometry[@Id = $elt/@id]"/> + <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 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"> + <xsl:attribute name="x"> + <xsl:value-of select="$g2/@x - $g1/@x"/> + <xsl:attribute name="y"> + <xsl:value-of select="$g2/@y - $g1/@y"/> + <func:result select="exsl:node-set($result)"/> + <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']"/> + <xsl:variable name="hmi_textlists" select="$hmi_elements[@id = $hmi_textlists_descs/@id]"/> + <xsl:variable name="hmi_textstylelists_descs" select="$parsed_widgets/widget[@type = 'TextStyleList']"/> + <xsl:variable name="hmi_textstylelists" select="$hmi_elements[@id = $hmi_textstylelists_descs/@id]"/> + <xsl:variable name="textstylelist_related"> + <xsl:for-each select="$hmi_textstylelists"> + <xsl:attribute name="listid"> + <xsl:value-of select="@id"/> + <xsl:for-each select="func:refered_elements(.)"> + <xsl:attribute name="eltid"> + <xsl:value-of select="@id"/> + <xsl:variable name="textstylelist_related_ns" select="exsl:node-set($textstylelist_related)"/> + <xsl:variable name="hmi_pages_descs" select="$parsed_widgets/widget[@type = 'Page']"/> + <xsl:variable name="hmi_pages" select="$hmi_elements[@id = $hmi_pages_descs/@id]"/> + <xsl:variable name="default_page"> + <xsl:when test="count($hmi_pages) > 1"> + <xsl:when test="$hmi_pages_descs/arg[1]/@value = 'Home'"> + <xsl:text>Home</xsl:text> + <xsl:message terminate="yes"> + <xsl:text>No Home page defined!</xsl:text> + <xsl:when test="count($hmi_pages) = 0"> + <xsl:message terminate="yes"> + <xsl:text>No page defined!</xsl:text> + <xsl:value-of select="func:widget($hmi_pages/@id)/arg[1]/@value"/> + <preamble:default-page/> + <xsl:template match="preamble:default-page"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:text>var default_page = "</xsl:text> + <xsl:value-of select="$default_page"/> + <xsl:variable name="screensaverpage" select="$hmi_pages_descs[arg[1]/@value = 'ScreenSaver']"/> + <xsl:variable name="delay"> + <xsl:when test="$screensaverpage"> + <xsl:variable name="delaystr" select="$screensaverpage/arg[2]/@value"/> + <xsl:if test="not(regexp:test($delaystr,'^[0-9]+$'))"> + <xsl:message terminate="yes"> + <xsl:text>ScreenSaver page has missing or malformed delay argument.</xsl:text> + <xsl:value-of select="$delaystr"/> + <xsl:text>null</xsl:text> + <xsl:text>var screensaver_delay = </xsl:text> + <xsl:value-of select="$delay"/> + <xsl:variable name="keypads_descs" select="$parsed_widgets/widget[@type = 'Keypad']"/> + <xsl:variable name="keypads" select="$hmi_elements[@id = $keypads_descs/@id]"/> + <func:function name="func:refered_elements"> + <xsl:param name="elems"/> + <xsl:variable name="descend" select="$elems/descendant-or-self::svg:*"/> + <xsl:variable name="clones" select="$descend[self::svg:use]"/> + <xsl:variable name="originals" select="//svg:*[concat('#',@id) = $clones/@xlink:href]"/> + <xsl:when test="$originals"> + <func:result select="$descend | func:refered_elements($originals)"/> + <func:result select="$descend"/> + <xsl:variable name="_overlapping_geometry"> + <xsl:for-each select="$hmi_pages | $keypads"> + <xsl:variable name="k" select="concat('overlapping:', @id)"/> + <xsl:value-of select="ns:ProgressStart($k, concat('collecting membership of ', @inkscape:label))"/> + <xsl:attribute name="id"> + <xsl:value-of select="@id"/> + <xsl:copy-of select="func:overlapping_geometry(.)"/> + <xsl:value-of select="ns:ProgressEnd($k)"/> + <xsl:variable name="overlapping_geometry" select="exsl:node-set($_overlapping_geometry)"/> + <func:function name="func:all_related_elements"> + <xsl:param name="page"/> + <xsl:variable name="page_overlapping_geometry" select="$overlapping_geometry/elt[@id = $page/@id]/*"/> + <xsl:variable name="overlapping_candidates" select="//svg:*[not(starts-with((ancestor::svg:g | .) /@inkscape:label, 'DISCARD:'))]"/> + <xsl:variable name="page_overlapping_elements" select="$overlapping_candidates[@id = $page_overlapping_geometry/@Id]"/> + <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 name="func:required_elements"> + <xsl:param name="pages"/> + <xsl:when test="$pages"> + <func:result select="func:all_related_elements($pages[1]) | func:required_elements($pages[position()!=1])"/> + <func:result select="/.."/> + <xsl:variable name="required_page_elements" select="func:required_elements($hmi_pages | $keypads)/ancestor-or-self::svg:*"/> + <xsl:variable name="required_list_elements" select="func:refered_elements(($hmi_lists | $hmi_textlists)[@id = $required_page_elements/@id])/ancestor-or-self::svg:*"/> + <xsl:variable name="required_elements" select="$defs | $required_list_elements | $required_page_elements"/> + <xsl:variable name="discardable_elements" select="//svg:*[not(@id = $required_elements/@id)]"/> + <func:function name="func:sumarized_elements"> + <xsl:param name="elements"/> + <xsl:variable name="short_list" select="$elements[not(ancestor::*/@id = $elements/@id)]"/> + <xsl:variable name="filled_groups" select="$short_list/parent::*[ not(child::*[ not(@id = $discardable_elements/@id) and not(@id = $short_list/@id) ])]"/> + <xsl:variable name="groups_to_add" select="$filled_groups[not(ancestor::*/@id = $filled_groups/@id)]"/> + <func:result select="$groups_to_add | $short_list[not(ancestor::*/@id = $filled_groups/@id)]"/> + <func:function name="func:detachable_elements"> + <xsl:param name="pages"/> + <xsl:when test="$pages"> + <func:result select="func:sumarized_elements(func:all_related_elements($pages[1])) | func:detachable_elements($pages[position()!=1])"/> + <func:result select="/.."/> + <xsl:variable name="_detachable_elements" select="func:detachable_elements($hmi_pages | $keypads)"/> + <xsl:variable name="detachable_elements" select="$_detachable_elements[not(ancestor::*/@id = $_detachable_elements/@id)]"/> + <declarations:page-class/> + <xsl:template match="declarations:page-class"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:text>class PageWidget extends Widget{} + <declarations:detachable-elements/> + <xsl:template match="declarations:detachable-elements"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:text>var detachable_elements = { + <xsl:for-each select="$detachable_elements"> + <xsl:text> "</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>":[id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>"), id("</xsl:text> + <xsl:value-of select="../@id"/> + <xsl:text>")]</xsl:text> + <xsl:if test="position()!=last()"> + <xsl:variable name="forEach_widgets_ids" select="$parsed_widgets/widget[@type = 'ForEach']/@id"/> + <xsl:variable name="forEach_widgets" select="$hmi_widgets[@id = $forEach_widgets_ids]"/> + <xsl:variable name="in_forEach_widget_ids" select="func:refered_elements($forEach_widgets)[not(@id = $forEach_widgets_ids)]/@id"/> + <xsl:template mode="page_desc" match="svg:*"> + <xsl:if test="ancestor::*[@id = $hmi_pages/@id]"> + <xsl:message terminate="yes"> + <xsl:text>HMI:Page </xsl:text> + <xsl:value-of select="@id"/> + <xsl:text> is nested in another HMI:Page</xsl:text> + <xsl:variable name="desc" select="func:widget(@id)"/> + <xsl:variable name="pagename" select="$desc/arg[1]/@value"/> + <xsl:variable name="msg" select="concat('generating page description ', $pagename)"/> + <xsl:value-of select="ns:ProgressStart($pagename, $msg)"/> + <xsl:variable name="page" select="."/> + <xsl:variable name="p" select="$geometry[@Id = $page/@id]"/> + <xsl:variable name="page_all_elements" select="func:all_related_elements($page)"/> + <xsl:variable name="all_page_widgets" select="$hmi_widgets[@id = $page_all_elements/@id and @id != $page/@id]"/> + <xsl:variable name="page_managed_widgets" select="$all_page_widgets[not(@id=$in_forEach_widget_ids)]"/> + <xsl:variable name="page_root_path" select="$desc/path[not(@assign)]"/> + <xsl:if test="count($page_root_path)>1"> + <xsl:message terminate="yes"> + <xsl:text>Page id="</xsl:text> + <xsl:value-of select="$page/@id"/> + <xsl:text>" : only one root path can be declared</xsl:text> + <xsl:variable name="page_relative_widgets" select="$page_managed_widgets[func:is_descendant_path(func:widget(@id)/path/@value, $page_root_path/@value)]"/> + <xsl:variable name="sumarized_page" select="func:sumarized_elements($page_all_elements)"/> + <xsl:variable name="required_detachables" select="$sumarized_page/ ancestor-or-self::*[@id = $detachable_elements/@id]"/> + <xsl:text> "</xsl:text> + <xsl:value-of select="$pagename"/> + <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:if test="count($page_root_path)=1"> + <xsl:if test="count($page_root_path/@index)=0"> + <xsl:message terminate="no"> + <xsl:text>Page id="</xsl:text> + <xsl:value-of select="$page/@id"/> + <xsl:text>" : No match for path "</xsl:text> + <xsl:value-of select="$page_root_path/@value"/> + <xsl:text>" in HMI tree</xsl:text> + <xsl:text> page_index: </xsl:text> + <xsl:value-of select="$page_root_path/@index"/> + <xsl:text> page_class: "</xsl:text> + <xsl:value-of select="$indexed_hmitree/*[@hmipath = $page_root_path/@value]/@class"/> + <xsl:text> [hmi_widgets["</xsl:text> + <xsl:value-of select="$page/@id"/> + <xsl:for-each select="$page_managed_widgets"> + <xsl:variable name="widget_paths_relativeness"> + <xsl:for-each select="func:widget(@id)/path"> + <xsl:value-of select="func:is_descendant_path(@value, $page_root_path/@value)"/> + <xsl:if test="position()!=last()"> + <xsl:text> [hmi_widgets["</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>"], [</xsl:text> + <xsl:value-of select="$widget_paths_relativeness"/> + <xsl:text>]]</xsl:text> + <xsl:if test="position()!=last()"> + <xsl:for-each select="$parsed_widgets/widget[@id = $all_page_widgets/@id and @type='Jump']"> + <xsl:text> hmi_widgets["</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>"]</xsl:text> + <xsl:if test="position()!=last()"> + <xsl:text> required_detachables: { + <xsl:for-each select="$required_detachables"> + <xsl:text> "</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>": detachable_elements["</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>"]</xsl:text> + <xsl:if test="position()!=last()"> + <xsl:apply-templates mode="widget_page" select="$parsed_widgets/widget[@id = $all_page_widgets/@id]"> + <xsl:with-param name="page_desc" select="$desc"/> + <xsl:text> }</xsl:text> + <xsl:if test="position()!=last()"> + <xsl:value-of select="ns:ProgressEnd($pagename)"/> + <definitions:page-desc/> + <xsl:template match="definitions:page-desc"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:text>var page_desc = { + <xsl:apply-templates mode="page_desc" select="$hmi_pages"/> + <xsl:template mode="widget_page" match="*"/> + <debug:detachable-pages/> + <xsl:template match="debug:detachable-pages"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:for-each select="$detachable_elements"> + <xsl:value-of select="@id"/> + <xsl:text>DISCARDABLES: + <xsl:for-each select="$discardable_elements"> + <xsl:value-of select="@id"/> + <xsl:for-each select="$in_forEach_widget_ids"> + <xsl:value-of select="."/> + <xsl:apply-templates mode="testtree" select="$overlapping_geometry"/> + <xsl:template xmlns="http://www.w3.org/2000/svg" mode="inline_svg" match="@*"> + <xsl:template mode="inline_svg" match="node()"> + <xsl:if test="not(@id = $discardable_elements/@id)"> + <xsl:apply-templates mode="inline_svg" select="@* | node()"/> + <xsl:template mode="inline_svg" match="svg:svg/@width"/> + <xsl:template mode="inline_svg" match="svg:svg/@height"/> + <xsl:template xmlns="http://www.w3.org/2000/svg" mode="inline_svg" match="svg:svg"> + <xsl:attribute name="preserveAspectRatio"> + <xsl:text>none</xsl:text> + <xsl:attribute name="height"> + <xsl:text>100vh</xsl:text> + <xsl:attribute name="width"> + <xsl:text>100vw</xsl:text> + <xsl:apply-templates mode="inline_svg" select="@* | node()"/> + <xsl:template mode="inline_svg" match="svg:svg[@viewBox!=concat('0 0 ', @width, ' ', @height)]"> + <xsl:message terminate="yes"> + <xsl:text>ViewBox settings other than X=0, Y=0 and Scale=1 are not supported</xsl:text> + <xsl:template mode="inline_svg" match="sodipodi:namedview[@units!='px' or @inkscape:document-units!='px']"> + <xsl:message terminate="yes"> + <xsl:text>All units must be set to "px" in Inkscape's document properties</xsl:text> + <xsl:template xmlns="http://www.w3.org/2000/svg" mode="inline_svg" match="svg:text/@inkscape:label[starts-with(., '_')]"> + <xsl:attribute name="{name()}"> + <xsl:value-of select="substring(., 2)"/> + <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:attribute name="svghmi_x_offset"> + <xsl:value-of select="$offset/vector/@x"/> + <xsl:attribute name="svghmi_y_offset"> + <xsl:value-of select="$offset/vector/@y"/> + <xsl:apply-templates mode="inline_svg" select="@* | node()"/> + <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"> + <xsl:param name="targetid"/> + <xsl:param name="eltid"/> + <func:result select="$eltid = $to_unlink/@id and not($targetid = $targets_not_to_unlink/@id)"/> + <xsl:template xmlns="http://www.w3.org/2000/svg" mode="inline_svg" match="svg:use"> + <xsl:variable name="targetid" select="substring-after(@xlink:href,'#')"/> + <xsl:when test="func:is_unlinkable($targetid, @id)"> + <xsl:call-template name="unlink_clone"> + <xsl:with-param name="targetid" select="$targetid"/> + <xsl:apply-templates mode="inline_svg" select="@*"/> + <xsl:variable name="_excluded_use_attrs"> + <xsl:text>href</xsl:text> + <xsl:text>width</xsl:text> + <xsl:text>height</xsl:text> + <xsl:text>id</xsl:text> + <xsl:variable name="excluded_use_attrs" select="exsl:node-set($_excluded_use_attrs)"/> + <xsl:variable name="_merge_use_attrs"> + <xsl:text>transform</xsl:text> + <xsl:text>style</xsl:text> + <xsl:variable name="merge_use_attrs" select="exsl:node-set($_merge_use_attrs)"/> + <xsl:template xmlns="http://www.w3.org/2000/svg" name="unlink_clone"> + <xsl:param name="targetid"/> + <xsl:param name="seed" select="''"/> + <xsl:variable name="target" select="//svg:*[@id = $targetid]"/> + <xsl:variable name="seeded_id"> + <xsl:when test="string-length($seed) > 0"> + <xsl:value-of select="$seed"/> + <xsl:value-of select="@id"/> + <xsl:value-of select="@id"/> + <xsl:attribute name="id"> + <xsl:value-of select="$seeded_id"/> + <xsl:attribute name="original"> + <xsl:value-of select="@id"/> + <xsl:when test="$target[self::svg:g]"> + <xsl:for-each select="@*[not(local-name() = $excluded_use_attrs/name | $merge_use_attrs)]"> + <xsl:attribute name="{name()}"> + <xsl:value-of select="."/> + <xsl:if test="@style | $target/@style"> + <xsl:attribute name="style"> + <xsl:value-of select="@style"/> + <xsl:if test="@style and $target/@style"> + <xsl:value-of select="$target/@style"/> + <xsl:if test="@transform | $target/@transform"> + <xsl:attribute name="transform"> + <xsl:value-of select="@transform"/> + <xsl:if test="@transform and $target/@transform"> + <xsl:value-of select="$target/@transform"/> + <xsl:apply-templates mode="unlink_clone" select="$target/*"> + <xsl:with-param name="seed" select="$seeded_id"/> + <xsl:for-each select="@*[not(local-name() = $excluded_use_attrs/name)]"> + <xsl:attribute name="{name()}"> + <xsl:value-of select="."/> + <xsl:apply-templates mode="unlink_clone" select="$target"> + <xsl:with-param name="seed" select="$seeded_id"/> + <xsl:template xmlns="http://www.w3.org/2000/svg" mode="unlink_clone" match="@id"> + <xsl:param name="seed"/> + <xsl:attribute name="id"> + <xsl:value-of select="$seed"/> + <xsl:value-of select="."/> + <xsl:attribute name="original"> + <xsl:value-of select="."/> + <xsl:template xmlns="http://www.w3.org/2000/svg" mode="unlink_clone" match="@*"> + <xsl:template xmlns="http://www.w3.org/2000/svg" mode="unlink_clone" match="svg:use"> + <xsl:param name="seed"/> + <xsl:variable name="targetid" select="substring-after(@xlink:href,'#')"/> + <xsl:when test="func:is_unlinkable($targetid, @id)"> + <xsl:call-template name="unlink_clone"> + <xsl:with-param name="targetid" select="$targetid"/> + <xsl:with-param name="seed" select="$seed"/> + <xsl:apply-templates mode="unlink_clone" select="@*"> + <xsl:with-param name="seed" select="$seed"/> + <xsl:template xmlns="http://www.w3.org/2000/svg" mode="unlink_clone" match="svg:*"> + <xsl:param name="seed"/> + <xsl:when test="@id = $hmi_widgets/@id"> + <xsl:attribute name="xlink:href"> + <xsl:value-of select="concat('#',@id)"/> + <xsl:apply-templates mode="unlink_clone" select="@* | node()"> + <xsl:with-param name="seed" select="$seed"/> + <xsl:variable name="result_svg"> + <xsl:apply-templates mode="inline_svg" select="/"/> + <xsl:variable name="result_svg_ns" select="exsl:node-set($result_svg)"/> + <xsl:template match="preamble:inline-svg"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:text>const xmlns = "http://www.w3.org/2000/svg"; + <xsl:text>let id = document.getElementById.bind(document); + <xsl:text>var svg_root = id("</xsl:text> + <xsl:value-of select="$svg/@id"/> + <debug:clone-unlinking/> + <xsl:template match="debug:clone-unlinking"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:for-each select="$to_unlink"> + <xsl:value-of select="@id"/> + <xsl:text>Not to unlink : + <xsl:for-each select="$targets_not_to_unlink"> + <xsl:value-of select="@id"/> + <xsl:template mode="extract_i18n" match="svg:tspan"> + <xsl:if test="string-length(.) > 0"> + <xsl:value-of select="."/> + <xsl:template mode="extract_i18n" match="svg:text"> + <xsl:attribute name="id"> + <xsl:value-of select="@id"/> + <xsl:attribute name="label"> + <xsl:value-of select="substring(@inkscape:label,2)"/> + <xsl:if test="string-length(text()) > 0"> + <xsl:value-of select="text()"/> + <xsl:apply-templates mode="extract_i18n" select="svg:*"/> + <xsl:variable name="translatable_texts" select="//svg:text[starts-with(@inkscape:label, '_')]"/> + <xsl:variable name="translatable_strings"> + <xsl:apply-templates mode="extract_i18n" select="$translatable_texts"/> + <xsl:template match="preamble:i18n"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:variable name="translations" select="ns:GetTranslations($translatable_strings)"/> + <xsl:text>var langs = [ ["Default", "C"],</xsl:text> + <xsl:for-each select="$translations/langs/lang"> + <xsl:text>["</xsl:text> + <xsl:value-of select="."/> + <xsl:text>","</xsl:text> + <xsl:value-of select="@code"/> + <xsl:text>"]</xsl:text> + <xsl:if test="position()!=last()"> + <xsl:text>var translations = [ + <xsl:for-each select="$translatable_texts"> + <xsl:variable name="n" select="position()"/> + <xsl:variable name="current_id" select="@id"/> + <xsl:variable name="text_unlinked_uses" select="$result_svg_ns//svg:text[@original = $current_id]/@id"/> + <xsl:text> [[</xsl:text> + <xsl:for-each select="@id | $text_unlinked_uses"> + <xsl:text>id("</xsl:text> + <xsl:value-of select="."/> + <xsl:text>")</xsl:text> + <xsl:if test="position()!=last()"> + <xsl:text>],[</xsl:text> + <xsl:for-each select="$translations/messages/msgid[$n]/msg"> + <xsl:for-each select="line"> + <xsl:value-of select="."/> + <xsl:if test="position()!=last()"> + <xsl:text>\n</xsl:text> + <xsl:if test="position()!=last()"> + <xsl:text>]]</xsl:text> + <xsl:if test="position()!=last()"> + <xsl:template mode="hmi_widgets" match="svg:*"> + <xsl:variable name="widget" select="func:widget(@id)"/> + <xsl:variable name="eltid" select="@id"/> + <xsl:variable name="args"> + <xsl:for-each select="$widget/arg"> + <xsl:value-of select="func:escape_quotes(@value)"/> + <xsl:if test="position()!=last()"> + <xsl:variable name="indexes"> + <xsl:for-each select="$widget/path"> + <xsl:if test="position()!=last()"> + <xsl:variable name="variables"> + <xsl:for-each select="$widget/path"> + <xsl:when test="not(@index)"> + <xsl:when test="not(@type)"> + <xsl:message terminate="no"> + <xsl:text>Widget </xsl:text> + <xsl:value-of select="$widget/@type"/> + <xsl:text> id="</xsl:text> + <xsl:value-of select="$eltid"/> + <xsl:text>" : No match for path "</xsl:text> + <xsl:value-of select="@value"/> + <xsl:text>" in HMI tree</xsl:text> + <xsl:text>undefined</xsl:text> + <xsl:when test="@type = 'PAGE_LOCAL'"> + <xsl:value-of select="@value"/> + <xsl:when test="@type = 'HMI_LOCAL'"> + <xsl:text>hmi_local_index("</xsl:text> + <xsl:value-of select="@value"/> + <xsl:text>")</xsl:text> + <xsl:message terminate="yes"> + <xsl:text>Internal error while processing widget's non indexed HMI tree path : unknown type</xsl:text> + <xsl:value-of select="@index"/> + <xsl:text>, {</xsl:text> + <xsl:if test="@min and @max"> + <xsl:text>minmax:["</xsl:text> + <xsl:value-of select="@min"/> + <xsl:text>", "</xsl:text> + <xsl:value-of select="@max"/> + <xsl:text>"]</xsl:text> + <xsl:if test="@assign"> + <xsl:if test="@assign"> + <xsl:text>assign:"</xsl:text> + <xsl:value-of select="@assign"/> + <xsl:text>}]</xsl:text> + <xsl:if test="position()!=last()"> + <xsl:variable name="freq"> + <xsl:when test="$widget/@freq"> + <xsl:value-of select="$widget/@freq"/> + <xsl:text>undefined</xsl:text> + <xsl:variable name="enable_expr"> + <xsl:when test="$widget/@enable_expr"> + <xsl:text>true</xsl:text> + <xsl:text>false</xsl:text> + <xsl:text> "</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>": new </xsl:text> + <xsl:value-of select="$widget/@type"/> + <xsl:text>Widget ("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>",</xsl:text> + <xsl:value-of select="$freq"/> + <xsl:text>,[</xsl:text> + <xsl:value-of select="$args"/> + <xsl:text>],[</xsl:text> + <xsl:value-of select="$variables"/> + <xsl:text>],</xsl:text> + <xsl:value-of select="$enable_expr"/> + <xsl:text> var_assignments: [], + <xsl:text> assignment_idx: { + <xsl:for-each select="$widget/path"> + <xsl:variable name="varid" select="generate-id()"/> + <xsl:if test="@assign"> + <xsl:for-each select="$widget/path[@assign]"> + <xsl:if test="$varid = generate-id()"> + <xsl:text> "</xsl:text> + <xsl:value-of select="@assign"/> + <xsl:text>":</xsl:text> + <xsl:value-of select="position()-1"/> + <xsl:if test="position()!=last()"> + <xsl:text> varnum_assignments: [ + <xsl:for-each select="$widget/path"> + <xsl:variable name="varid" select="generate-id()"/> + <xsl:when test="@assign"> + <xsl:for-each select="$widget/path[@assign]"> + <xsl:if test="$varid = generate-id()"> + <xsl:value-of select="position()-1"/> + <xsl:text> undefined</xsl:text> + <xsl:if test="position()!=last()"> + <xsl:if test="$widget/@enable_expr"> + <xsl:text> compute_enable: function(value, oldval, varnum) { + <xsl:text> let result = false; + <xsl:for-each select="$widget/path[@assign]"> + <xsl:text> let </xsl:text> + <xsl:value-of select="@assign"/> + <xsl:text> = this.var_assignments[</xsl:text> + <xsl:value-of select="position()-1"/> + <xsl:text> if(</xsl:text> + <xsl:value-of select="@assign"/> + <xsl:text> == undefined) break; + <xsl:text> result = </xsl:text> + <xsl:value-of select="$widget/@enable_expr"/> + <xsl:text> this.enable(result); + <xsl:apply-templates mode="widget_defs" select="$widget"> + <xsl:with-param name="hmi_element" select="."/> + <xsl:text> })</xsl:text> + <xsl:if test="position()!=last()"> + <preamble:local-variable-indexes/> + <xsl:template match="preamble:local-variable-indexes"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:text>let hmi_locals = {}; + <xsl:text>var last_remote_index = hmitree_types.length - 1; + <xsl:text>var next_available_index = hmitree_types.length; + <xsl:text>let cookies = new Map(document.cookie.split("; ").map(s=>s.split("="))); + <xsl:text>const local_defaults = { + <xsl:for-each select="$parsed_widgets/widget[starts-with(@type,'VarInit')]"> + <xsl:if test="count(path) != 1"> + <xsl:message terminate="yes"> + <xsl:text>VarInit </xsl:text> + <xsl:value-of select="@id"/> + <xsl:text> must have only one variable given.</xsl:text> + <xsl:if test="path/@type != 'PAGE_LOCAL' and path/@type != 'HMI_LOCAL'"> + <xsl:message terminate="yes"> + <xsl:text>VarInit </xsl:text> + <xsl:value-of select="@id"/> + <xsl:text> only applies to HMI variable.</xsl:text> + <xsl:text> "</xsl:text> + <xsl:value-of select="path/@value"/> + <xsl:text>":</xsl:text> + <xsl:when test="@type = 'VarInitPersistent'"> + <xsl:text>cookies.has("</xsl:text> + <xsl:value-of select="path/@value"/> + <xsl:text>")?cookies.get("</xsl:text> + <xsl:value-of select="path/@value"/> + <xsl:text>"):</xsl:text> + <xsl:value-of select="arg[1]/@value"/> + <xsl:value-of select="arg[1]/@value"/> + <xsl:if test="position()!=last()"> + <xsl:text>const persistent_locals = new Set([ + <xsl:for-each select="$parsed_widgets/widget[@type='VarInitPersistent']"> + <xsl:text> "</xsl:text> + <xsl:value-of select="path/@value"/> + <xsl:if test="position()!=last()"> + <xsl:text>var persistent_indexes = new Map(); + <xsl:text>var cache = hmitree_types.map(_ignored => undefined); + <xsl:text>function page_local_index(varname, pagename){ + <xsl:text> let pagevars = hmi_locals[pagename]; + <xsl:text> let new_index; + <xsl:text> if(pagevars == undefined){ + <xsl:text> new_index = next_available_index++; + <xsl:text> hmi_locals[pagename] = {[varname]:new_index}; + <xsl:text> let result = pagevars[varname]; + <xsl:text> if(result != undefined) { + <xsl:text> return result; + <xsl:text> new_index = next_available_index++; + <xsl:text> pagevars[varname] = new_index; + <xsl:text> let defaultval = local_defaults[varname]; + <xsl:text> if(defaultval != undefined) { + <xsl:text> cache[new_index] = defaultval; + <xsl:text> if(persistent_locals.has(varname)) + <xsl:text> persistent_indexes.set(new_index, varname); + <xsl:text> return new_index; + <xsl:text>function hmi_local_index(varname){ + <xsl:text> return page_local_index(varname, "HMI_LOCAL"); + <preamble:widget-base-class/> + <xsl:template match="preamble:widget-base-class"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:text>var pending_widget_animates = []; + <xsl:text>function _hide(elt, placeholder){ + <xsl:text> if(elt.parentNode != null) + <xsl:text> placeholder.parentNode.removeChild(elt); + <xsl:text>function _show(elt, placeholder){ + <xsl:text> placeholder.parentNode.insertBefore(elt, placeholder); + <xsl:text>function set_activity_state(eltsub, state){ + <xsl:text> if(eltsub.active_elt != undefined){ + <xsl:text> if(eltsub.active_elt_placeholder == undefined){ + <xsl:text> eltsub.active_elt_placeholder = document.createComment(""); + <xsl:text> eltsub.active_elt.parentNode.insertBefore(eltsub.active_elt_placeholder, eltsub.active_elt); + <xsl:text> (state?_show:_hide)(eltsub.active_elt, eltsub.active_elt_placeholder); + <xsl:text> if(eltsub.inactive_elt != undefined){ + <xsl:text> if(eltsub.inactive_elt_placeholder == undefined){ + <xsl:text> eltsub.inactive_elt_placeholder = document.createComment(""); + <xsl:text> eltsub.inactive_elt.parentNode.insertBefore(eltsub.inactive_elt_placeholder, eltsub.inactive_elt); + <xsl:text> ((state || state==undefined)?_hide:_show)(eltsub.inactive_elt, eltsub.inactive_elt_placeholder); + <xsl:text>class Widget { + <xsl:text> frequency = 10; /* FIXME arbitrary default max freq. Obtain from config ? */ + <xsl:text> unsubscribable = false; + <xsl:text> pending_animate = false; + <xsl:text> constructor(elt_id, freq, args, variables, enable_expr, members){ + <xsl:text> this.element_id = elt_id; + <xsl:text> this.element = id(elt_id); + <xsl:text> this.args = args; + <xsl:text> [this.indexes, this.variables_options] = (variables.length>0) ? zip(...variables) : [[],[]]; + <xsl:text> this.indexes_length = this.indexes.length; + <xsl:text> this.enable_expr = enable_expr; + <xsl:text> this.enable_state = true; + <xsl:text> this.enable_displayed_state = true; + <xsl:text> this.enabled_elts = []; + <xsl:text> Object.keys(members).forEach(prop => this[prop]=members[prop]); + <xsl:text> this.lastapply = this.indexes.map(() => undefined); + <xsl:text> this.inhibit = this.indexes.map(() => undefined); + <xsl:text> this.pending = this.indexes.map(() => undefined); + <xsl:text> this.bound_uninhibit = this.uninhibit.bind(this); + <xsl:text> this.lastdispatch = this.indexes.map(() => undefined); + <xsl:text> this.deafen = this.indexes.map(() => undefined); + <xsl:text> this.incoming = this.indexes.map(() => undefined); + <xsl:text> this.bound_undeafen = this.undeafen.bind(this); + <xsl:text> this.forced_frequency = freq; + <xsl:text> this.clip = true; + <xsl:text> let forced = this.forced_frequency; + <xsl:text> if(forced !== undefined){ + <xsl:text> once every 10 seconds : 10s + <xsl:text> once per minute : 1m + <xsl:text> once per hour : 1h + <xsl:text> once per day : 1d + <xsl:text> let unit = forced.slice(-1); + <xsl:text> let factor = { + <xsl:text> "d":86400}[unit]; + <xsl:text> this.frequency = factor ? 1/(factor * Number(forced.slice(0,-1))) + <xsl:text> : Number(forced); + <xsl:text> let init = this.init; + <xsl:text> if(typeof(init) == "function"){ + <xsl:text> init.call(this); + <xsl:text> } catch(err) { + <xsl:text> console.log(err); + <xsl:text> if(this.enable_expr){ + <xsl:text> this.enable_state = false; + <xsl:text> this.enable_displayed_state = false; + <xsl:text> for(let child of Array.from(this.element.children)){ + <xsl:text> let label = child.getAttribute("inkscape:label"); + <xsl:text> if(label!="disabled"){ + <xsl:text> this.enabled_elts.push(child); + <xsl:text> this.element.removeChild(child); + <xsl:text> /* remove subsribers */ + <xsl:text> for(let i = 0; i < this.indexes_length; i++) { + <xsl:text> /* flush updates pending because of inhibition */ + <xsl:text> let inhibition = this.inhibit[i]; + <xsl:text> if(inhibition != undefined){ + <xsl:text> clearTimeout(inhibition); + <xsl:text> this.lastapply[i] = undefined; + <xsl:text> this.uninhibit(i); + <xsl:text> let deafened = this.deafen[i]; + <xsl:text> if(deafened != undefined){ + <xsl:text> clearTimeout(deafened); + <xsl:text> this.lastdispatch[i] = undefined; + <xsl:text> this.undeafen(i); + <xsl:text> let index = this.get_variable_index(i); + <xsl:text> subscribers(index).delete(this); + <xsl:text> this.offset = 0; + <xsl:text> this.relativeness = undefined; + <xsl:text> sub(new_offset, relativeness, container_id){ + <xsl:text> this.offset = new_offset; + <xsl:text> this.relativeness = relativeness; + <xsl:text> this.container_id = container_id ; + <xsl:text> /* add this's subsribers */ + <xsl:text> for(let i = 0; i < this.indexes_length; i++) { + <xsl:text> let index = this.get_variable_index(i); + <xsl:text> if(index == undefined) continue; + <xsl:text> subscribers(index).add(this); + <xsl:text> this.apply_cache(); + <xsl:text> apply_cache() { + <xsl:text> for(let i = 0; i < this.indexes_length; i++) { + <xsl:text> /* dispatch current cache in newly opened page widgets */ + <xsl:text> let realindex = this.get_variable_index(i); + <xsl:text> if(realindex == undefined) continue; + <xsl:text> let cached_val = cache[realindex]; + <xsl:text> if(cached_val != undefined) + <xsl:text> this.feed_data_for_dispatch(cached_val, cached_val, i); + <xsl:text> get_variable_index(varnum) { + <xsl:text> let index = this.indexes[varnum]; + <xsl:text> if(typeof(index) == "string"){ + <xsl:text> index = page_local_index(index, this.container_id); + <xsl:text> if(this.relativeness[varnum]){ + <xsl:text> index += this.offset; + <xsl:text> return index; + <xsl:text> overshot(new_val, max) { + <xsl:text> undershot(new_val, min) { + <xsl:text> clip_min_max(index, new_val) { + <xsl:text> let minmax = this.variables_options[index].minmax; + <xsl:text> if(minmax !== undefined && typeof new_val == "number") { + <xsl:text> let [min,max] = minmax.map(token => { + <xsl:text> const num = Number(token); + <xsl:text> if(!isNaN(num) && isFinite(num)){ + <xsl:text> let idx = this.assignment_idx[token]; + <xsl:text> if(idx != undefined) + <xsl:text> return this.var_assignments[idx]; + <xsl:text> if(new_val < min){ + <xsl:text> this.undershot(new_val, min); + <xsl:text> if(new_val > max){ + <xsl:text> this.overshot(new_val, max); + <xsl:text> return new_val; + <xsl:text> change_hmi_value(index, opstr) { + <xsl:text> let realindex = this.get_variable_index(index); + <xsl:text> if(realindex == undefined) return undefined; + <xsl:text> let old_val = cache[realindex]; + <xsl:text> let new_val = eval_operation_string(old_val, opstr); + <xsl:text> if(this.clip) + <xsl:text> new_val = this.clip_min_max(index, new_val); + <xsl:text> return apply_hmi_value(realindex, new_val); + <xsl:text> _apply_hmi_value(index, new_val) { + <xsl:text> let realindex = this.get_variable_index(index); + <xsl:text> if(realindex == undefined) return undefined; + <xsl:text> if(this.clip) + <xsl:text> new_val = this.clip_min_max(index, new_val); + <xsl:text> return apply_hmi_value(realindex, new_val); + <xsl:text> uninhibit(index){ + <xsl:text> this.inhibit[index] = undefined; + <xsl:text> let new_val = this.pending[index]; + <xsl:text> this.pending[index] = undefined; + <xsl:text> return this.apply_hmi_value(index, new_val); + <xsl:text> apply_hmi_value(index, new_val) { + <xsl:text> if(this.inhibit[index] == undefined){ + <xsl:text> let now = Date.now(); + <xsl:text> let min_interval = 1000/this.frequency; + <xsl:text> let lastapply = this.lastapply[index]; + <xsl:text> if(lastapply == undefined || now > lastapply + min_interval){ + <xsl:text> this.lastapply[index] = now; + <xsl:text> return this._apply_hmi_value(index, new_val); + <xsl:text> let elapsed = now - lastapply; + <xsl:text> this.pending[index] = new_val; + <xsl:text> this.inhibit[index] = setTimeout(this.bound_uninhibit, min_interval - elapsed, index); + <xsl:text> this.pending[index] = new_val; + <xsl:text> return new_val; + <xsl:text> new_hmi_value(index, value, oldval) { + <xsl:text> // TODO avoid searching, store index at sub() + <xsl:text> for(let i = 0; i < this.indexes_length; i++) { + <xsl:text> let refindex = this.get_variable_index(i); + <xsl:text> if(refindex == undefined) continue; + <xsl:text> if(index == refindex) { + <xsl:text> this.feed_data_for_dispatch(value, oldval, i); + <xsl:text> undeafen(index){ + <xsl:text> this.deafen[index] = undefined; + <xsl:text> let [new_val, old_val] = this.incoming[index]; + <xsl:text> this.incoming[index] = undefined; + <xsl:text> this.lastdispatch[index] = Date.now(); + <xsl:text> this.do_dispatch(new_val, old_val, index); + <xsl:text> enable(enabled){ + <xsl:text> if(this.enable_state != enabled){ + <xsl:text> this.enable_state = enabled; + <xsl:text> this.request_animate(); + <xsl:text> animate_enable(){ + <xsl:text> if(this.enable_state && !this.enable_displayed_state){ + <xsl:text> //show widget + <xsl:text> for(let child of this.enabled_elts){ + <xsl:text> this.element.appendChild(child); + <xsl:text> //hide disabled content + <xsl:text> if(this.disabled_elt && this.disabled_elt.parentNode != null) + <xsl:text> this.element.removeChild(this.disabled_elt); + <xsl:text> this.enable_displayed_state = true; + <xsl:text> }else if(!this.enable_state && this.enable_displayed_state){ + <xsl:text> //hide widget + <xsl:text> for(let child of this.enabled_elts){ + <xsl:text> if(child.parentNode != null) + <xsl:text> this.element.removeChild(child); + <xsl:text> //show disabled content + <xsl:text> if(this.disabled_elt) + <xsl:text> this.element.appendChild(this.disabled_elt); + <xsl:text> this.enable_displayed_state = false; + <xsl:text> // once disabled activity display is lost + <xsl:text> this.activity_displayed_state = undefined; + <xsl:text> feed_data_for_dispatch(value, oldval, varnum) { + <xsl:text> if(this.dispatch || this.enable_expr){ + <xsl:text> if(this.deafen[varnum] == undefined){ + <xsl:text> let now = Date.now(); + <xsl:text> let min_interval = 1000/this.frequency; + <xsl:text> let lastdispatch = this.lastdispatch[varnum]; + <xsl:text> if(lastdispatch == undefined || now > lastdispatch + min_interval){ + <xsl:text> this.lastdispatch[varnum] = now; + <xsl:text> this.do_dispatch(value, oldval, varnum) + <xsl:text> let elapsed = now - lastdispatch; + <xsl:text> this.incoming[varnum] = [value, oldval]; + <xsl:text> this.deafen[varnum] = setTimeout(this.bound_undeafen, min_interval - elapsed, varnum); + <xsl:text> this.incoming[varnum] = [value, oldval]; + <xsl:text> do_dispatch(value, oldval, varnum) { + <xsl:text> if(this.enable_expr || this.dispatch){ + <xsl:text> let idx = this.varnum_assignments[varnum]; + <xsl:text> if(idx != undefined) + <xsl:text> this.var_assignments[idx] = value; + <xsl:text> if(this.dispatch) try { + <xsl:text> this.dispatch(value, oldval, varnum); + <xsl:text> } catch(err) { + <xsl:text> console.log(err); + <xsl:text> if(idx != undefined && this.enable_expr) try { + <xsl:text> this.compute_enable(value, oldval, varnum); + <xsl:text> } catch(err) { + <xsl:text> console.log(err); + <xsl:text> if(this.enable_expr) + <xsl:text> this.animate_enable(); + <xsl:text> // inhibit widget animation when disabled + <xsl:text> if(!this.enable_expr || this.enable_state){ + <xsl:text> if(this.has_activity) + <xsl:text> this.animate_activity(); + <xsl:text> if(this.animate != undefined) + <xsl:text> this.animate(); + <xsl:text> this.pending_animate = false; + <xsl:text> request_animate(){ + <xsl:text> if(!this.pending_animate){ + <xsl:text> pending_widget_animates.push(this); + <xsl:text> this.pending_animate = true; + <xsl:text> requestHMIAnimation(); + <xsl:text> animate_activity(){ + <xsl:text> if(this.activity_displayed_state != this.activity_state){ + <xsl:text> set_activity_state(this.activable_sub, this.activity_state); + <xsl:text> this.activity_displayed_state = this.activity_state; + <xsl:variable name="excluded_types" select="str:split('Page VarInit VarInitPersistent')"/> + <xsl:key name="TypesKey" match="widget" use="@type"/> + <declarations:hmi-classes/> + <xsl:template match="declarations:hmi-classes"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:variable name="used_widget_types" select="$parsed_widgets/widget[ generate-id() = generate-id(key('TypesKey', @type)) and not(@type = $excluded_types)]"/> + <xsl:apply-templates mode="widget_class" select="$used_widget_types"/> + <xsl:template mode="widget_class" match="widget"> + <xsl:text>class </xsl:text> + <xsl:value-of select="@type"/> + <xsl:text>Widget extends Widget{ + <xsl:text> /* empty class, as </xsl:text> + <xsl:value-of select="@type"/> + <xsl:text> widget didn't provide any */ + <xsl:message terminate="no"> + <xsl:value-of select="@type"/> + <xsl:text> widget is used in SVG but widget type is not declared</xsl:text> + <xsl:variable name="included_ids" select="$parsed_widgets/widget[not(@type = $excluded_types) and not(@id = $discardable_elements/@id)]/@id"/> + <xsl:variable name="page_ids" select="$parsed_widgets/widget[@type = 'Page']/@id"/> + <xsl:variable name="hmi_widgets" select="$hmi_elements[@id = $included_ids]"/> + <xsl:variable name="page_widgets" select="$hmi_elements[@id = $page_ids]"/> + <xsl:variable name="result_widgets" select="$result_svg_ns//*[@id = $hmi_widgets/@id]"/> + <declarations:hmi-elements/> + <xsl:template match="declarations:hmi-elements"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:text>var hmi_widgets = { + <xsl:apply-templates mode="hmi_widgets" select="$hmi_widgets | $page_widgets"/> + <xsl:template name="defs_by_labels"> + <xsl:param name="labels" select="''"/> + <xsl:param name="mandatory" select="'yes'"/> + <xsl:param name="subelements" select="/.."/> + <xsl:param name="hmi_element"/> + <xsl:variable name="widget_type" select="@type"/> + <xsl:variable name="widget_id" select="@id"/> + <xsl:for-each select="str:split($labels)"> + <xsl:variable name="absolute" select="starts-with(., '/')"/> + <xsl:variable name="name" select="substring(.,number($absolute)+1)"/> + <xsl:variable name="widget" select="$result_widgets[@id = $hmi_element/@id]"/> + <xsl:variable name="elt" select="($widget//*[not($absolute) and @inkscape:label=$name] | $widget/*[$absolute and @inkscape:label=$name])[1]"/> + <xsl:when test="not($elt/@id)"> + <xsl:if test="$mandatory!='no'"> + <xsl:variable name="errmsg"> + <xsl:value-of select="$widget_type"/> + <xsl:text> widget (id=</xsl:text> + <xsl:value-of select="$widget_id"/> + <xsl:text>) must have a </xsl:text> + <xsl:value-of select="$name"/> + <xsl:text> element</xsl:text> + <xsl:when test="$mandatory='yes'"> + <xsl:message terminate="yes"> + <xsl:value-of select="$errmsg"/> + <xsl:message terminate="no"> + <xsl:value-of select="$errmsg"/> + <xsl:value-of select="$name"/> + <xsl:text>_elt: id("</xsl:text> + <xsl:value-of select="$elt/@id"/> + <xsl:if test="$subelements"> + <xsl:value-of select="$name"/> + <xsl:for-each select="str:split($subelements)"> + <xsl:variable name="subname" select="."/> + <xsl:variable name="subelt" select="$elt/*[@inkscape:label=$subname][1]"/> + <xsl:when test="not($subelt/@id)"> + <xsl:if test="$mandatory!='no'"> + <xsl:variable name="errmsg"> + <xsl:value-of select="$widget_type"/> + <xsl:text> widget (id=</xsl:text> + <xsl:value-of select="$widget_id"/> + <xsl:text>) must have a </xsl:text> + <xsl:value-of select="$name"/> + <xsl:value-of select="$subname"/> + <xsl:text> element</xsl:text> + <xsl:when test="$mandatory='yes'"> + <xsl:message terminate="yes"> + <xsl:value-of select="$errmsg"/> + <xsl:message terminate="no"> + <xsl:value-of select="$errmsg"/> + <xsl:text> /* missing </xsl:text> + <xsl:value-of select="$name"/> + <xsl:value-of select="$subname"/> + <xsl:text> "</xsl:text> + <xsl:value-of select="$subname"/> + <xsl:text>_elt": id("</xsl:text> + <xsl:value-of select="$subelt/@id"/> + <xsl:text>")</xsl:text> + <xsl:if test="position()!=last()"> + <func:function name="func:escape_quotes"> + <xsl:param name="txt"/> + <xsl:when test="contains($txt,'"')"> + <func:result select="concat(substring-before($txt,'"'),'\"',func:escape_quotes(substring-after($txt,'"')))"/> + <func:result select="$txt"/> + <xsl:template match="widget[@type='Assign']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Arguments are either: + <xsl:text>- name=value: setting variable with literal value. + <xsl:text>- name=other_name: copy variable content into another + <xsl:text>"active"+"inactive" labeled elements can be provided to show feedback when pressed + <xsl:text>HMI:Assign:notify=1@notify=/PLCVAR + <xsl:text>HMI:Assign:ack=2:notify=1@ack=.local_var@notify=/PLCVAR + <xsl:text>Assign variables on click</xsl:text> + <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> frequency = 2; + <xsl:text> onmouseup(evt) { + <xsl:text> svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); + <xsl:text> if(this.enable_state) { + <xsl:text> this.activity_state = false + <xsl:text> this.request_animate(); + <xsl:text> this.assign(); + <xsl:text> onmousedown(){ + <xsl:text> if(this.enable_state) { + <xsl:text> svg_root.addEventListener("pointerup", this.bound_onmouseup, true); + <xsl:text> this.activity_state = true; + <xsl:text> this.request_animate(); + <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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:text> activable_sub:{ + <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 name="mandatory"> + <xsl:text>no</xsl:text> + <xsl:value-of select="$activity"/> + <xsl:variable name="has_activity" select="string-length($activity)>0"/> + <xsl:text> has_activity: </xsl:text> + <xsl:value-of select="$has_activity"/> + <xsl:text> init: function() { + <xsl:text> this.bound_onmouseup = this.onmouseup.bind(this); + <xsl:text> this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); + <xsl:text> assignments: {}, + <xsl:text> dispatch: function(value, oldval, varnum) { + <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> assign: function() { + <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:variable name="isVarName" select="regexp:test($value,'^[a-zA-Z_][a-zA-Z0-9_]*$')"/> + <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> if(</xsl:text> + <xsl:value-of select="$value"/> + <xsl:text> != undefined) + <xsl:text> this.apply_hmi_value(</xsl:text> + <xsl:value-of select="$index"/> + <xsl:text>, </xsl:text> + <xsl:value-of select="$value"/> + <xsl:text> this.apply_hmi_value(</xsl:text> + <xsl:value-of select="$index"/> + <xsl:text>, </xsl:text> + <xsl:value-of select="$value"/> + <xsl:template match="widget[@type='Back']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Back widget brings focus back to previous page in history when clicked. + <xsl:text>"active" + "inactive" labeled elements can be provided and reflect whether + <xsl:text>widget is pressed or not. + <xsl:text>Jump to previous page</xsl:text> + <xsl:template match="widget[@type='Back']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>BackWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> onmouseup(evt) { + <xsl:text> svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); + <xsl:text> this.activity_state = false; + <xsl:text> this.request_animate(); + <xsl:text> let page_name, index; + <xsl:text> if (jump_history.length > 1) { + <xsl:text> jump_history.pop(); // forget current page + <xsl:text> if (jump_history.length == 0) return; + <xsl:text> [page_name, index] = jump_history[jump_history.length-1]; + <xsl:text> } while (page_name == "ScreenSaver") // never go back to ScreenSaver + <xsl:text> fading_page_switch(page_name, index); + <xsl:text> onmousedown(){ + <xsl:text> svg_root.addEventListener("pointerup", this.bound_onmouseup, true); + <xsl:text> this.activity_state = true; + <xsl:text> this.request_animate(); + <xsl:text> this.bound_onmouseup = this.onmouseup.bind(this); + <xsl:text> this.activity_state = false; + <xsl:text> this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); + <xsl:template match="widget[@type='Back']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:text> activable_sub:{ + <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 name="mandatory"> + <xsl:text>no</xsl:text> + <xsl:value-of select="$activity"/> + <xsl:variable name="has_activity" select="string-length($activity)>0"/> + <xsl:text> has_activity: </xsl:text> + <xsl:value-of select="$has_activity"/> + <xsl:template match="widget[@type='Button']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Button widget takes one boolean variable path, and reflect current true + <xsl:text>or false value by showing "active" or "inactive" labeled element + <xsl:text>respectively. Pressing and releasing button changes variable to true and + <xsl:text>false respectively. Potential inconsistency caused by quick consecutive + <xsl:text>presses on the button is mitigated by using a state machine that wait for + <xsl:text>previous state change to be reflected on variable before applying next one. + <xsl:text>Push button reflecting consistently given boolean variable</xsl:text> + <path name="value" accepts="HMI_BOOL"> + <xsl:text>Boolean variable</xsl:text> + <xsl:variable name="_push_button_fsm"> + <on-dispatch value="false"> + <jump state="reflect_off"/> + <on-dispatch value="true"> + <jump state="reflect_on"/> + <state name="reflect_on"> + <show eltname="active"/> + <on-mouse position="down"> + <on-mouse position="up"> + <on-dispatch value="false"> + <jump state="reflect_off"/> + <hmi-value value="true"/> + <show eltname="active"/> + <on-mouse position="up"> + <on-dispatch value="false"> + <jump state="reflect_off"/> + <state name="reflect_off"> + <show eltname="inactive"/> + <on-mouse position="down"> + <on-mouse position="up"> + <on-dispatch value="true"> + <jump state="reflect_on"/> + <hmi-value value="false"/> + <show eltname="inactive"/> + <on-mouse position="down"> + <on-dispatch value="true"> + <jump state="reflect_on"/> + <xsl:variable name="_button_fsm"> + <on-dispatch value="false"> + <jump state="released"/> + <on-dispatch value="true"> + <jump state="pressed"/> + <state name="pressing"> + <hmi-value value="true"/> + <on-dispatch value="true"> + <jump state="pressed"/> + <on-mouse position="up"> + <jump state="shortpress"/> + <show eltname="active"/> + <on-mouse position="up"> + <jump state="releasing"/> + <on-dispatch value="false"> + <jump state="released"/> + <state name="shortpress"> + <on-dispatch value="true"> + <jump state="releasing"/> + <on-mouse position="down"> + <jump state="pressing"/> + <state name="releasing"> + <hmi-value value="false"/> + <on-dispatch value="false"> + <jump state="released"/> + <on-mouse position="down"> + <jump state="shortrelease"/> + <state name="released"> + <show eltname="inactive"/> + <on-mouse position="down"> + <jump state="pressing"/> + <on-dispatch value="true"> + <jump state="pressed"/> + <state name="shortrelease"> + <on-dispatch value="false"> + <jump state="pressing"/> + <on-mouse position="up"> + <jump state="releasing"/> + <xsl:template mode="dispatch_transition" match="fsm"> + <xsl:text> switch (this.state) { + <xsl:apply-templates mode="dispatch_transition" select="state"/> + <xsl:template mode="dispatch_transition" match="state"> + <xsl:text> case "</xsl:text> + <xsl:value-of select="@name"/> + <xsl:apply-templates select="on-dispatch"/> + <xsl:template match="on-dispatch"> + <xsl:text> if(value == </xsl:text> + <xsl:value-of select="@value"/> + <xsl:apply-templates mode="transition" select="jump"/> + <xsl:template mode="mouse_transition" match="fsm"> + <xsl:param name="position"/> + <xsl:text> switch (this.state) { + <xsl:apply-templates mode="mouse_transition" select="state"> + <xsl:with-param name="position" select="$position"/> + <xsl:template mode="mouse_transition" match="state"> + <xsl:param name="position"/> + <xsl:text> case "</xsl:text> + <xsl:value-of select="@name"/> + <xsl:apply-templates select="on-mouse[@position = $position]"/> + <xsl:template match="on-mouse"> + <xsl:apply-templates mode="transition" select="jump"/> + <xsl:template mode="transition" match="jump"> + <xsl:text> this.state = "</xsl:text> + <xsl:value-of select="@state"/> + <xsl:text> this.</xsl:text> + <xsl:value-of select="@state"/> + <xsl:template mode="actions" match="fsm"> + <xsl:apply-templates mode="actions" select="state"/> + <xsl:template mode="actions" match="state"> + <xsl:value-of select="@name"/> + <xsl:apply-templates mode="actions" select="*"/> + <xsl:template mode="actions" match="show"> + <xsl:text> this.activity_state = </xsl:text> + <xsl:value-of select="@eltname = 'active'"/> + <xsl:text> this.request_animate(); + <xsl:template mode="actions" match="hmi-value"> + <xsl:text> this.apply_hmi_value(0, </xsl:text> + <xsl:value-of select="@value"/> + <xsl:template name="generated_button_class"> + <xsl:param name="fsm"/> + <xsl:text> state = "init"; + <xsl:text> dispatch(value) { + <xsl:apply-templates mode="dispatch_transition" select="$fsm"/> + <xsl:text> onmouseup(evt) { + <xsl:text> svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); + <xsl:apply-templates mode="mouse_transition" select="$fsm"> + <xsl:with-param name="position" select="'up'"/> + <xsl:text> onmousedown(evt) { + <xsl:text> svg_root.addEventListener("pointerup", this.bound_onmouseup, true); + <xsl:apply-templates mode="mouse_transition" select="$fsm"> + <xsl:with-param name="position" select="'down'"/> + <xsl:apply-templates mode="actions" select="$fsm"/> + <xsl:text> this.bound_onmouseup = this.onmouseup.bind(this); + <xsl:text> this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); + <xsl:text> this.activity_state = undefined; + <xsl:template match="widget[@type='Button']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>ButtonWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 5; + <xsl:variable name="fsm" select="exsl:node-set($_button_fsm)"/> + <xsl:call-template name="generated_button_class"> + <xsl:with-param name="fsm" select="$fsm"/> + <xsl:template match="widget[@type='Button']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:text> activable_sub:{ + <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 name="mandatory"> + <xsl:text>warn</xsl:text> + <xsl:value-of select="$activity"/> + <xsl:variable name="has_activity" select="string-length($activity)>0"/> + <xsl:text> has_activity: </xsl:text> + <xsl:value-of select="$has_activity"/> + <xsl:template match="widget[@type='FlatButton']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>FlatButtonWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 5; + <xsl:variable name="fsm" select="exsl:node-set($_button_fsm)"/> + <xsl:call-template name="generated_button_class"> + <xsl:with-param name="fsm" select="$fsm"/> + <xsl:template match="widget[@type='FlatButton']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:template match="widget[@type='PushButton']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>PushButtonWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 20; + <xsl:variable name="fsm" select="exsl:node-set($_push_button_fsm)"/> + <xsl:call-template name="generated_button_class"> + <xsl:with-param name="fsm" select="$fsm"/> + <xsl:template match="widget[@type='PushButton']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:text> activable_sub:{ + <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 name="mandatory"> + <xsl:text>warn</xsl:text> + <xsl:value-of select="$activity"/> + <xsl:variable name="has_activity" select="string-length($activity)>0"/> + <xsl:text> has_activity: </xsl:text> + <xsl:value-of select="$has_activity"/> + <xsl:template match="widget[@type='CircularBar']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>CircularBar widget changes the end angle of a "path" labeled arc according + <xsl:text>to value of the single accepted variable. + <xsl:text>If "min" a "max" labeled texts are provided, then they are used as + <xsl:text>respective minimum and maximum value. Otherwise, value is expected to be + <xsl:text>in between 0 and 100. + <xsl:text>Change end angle of Inkscape's arc</xsl:text> + <arg name="min" count="optional" accepts="int,real"> + <xsl:text>minimum value</xsl:text> + <arg name="max" count="optional" accepts="int,real"> + <xsl:text>maximum value</xsl:text> + <path name="value" accepts="HMI_INT,HMI_REAL"> + <xsl:text>Value to display</xsl:text> + <xsl:template match="widget[@type='CircularBar']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>CircularBarWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 10; + <xsl:text> range = undefined; + <xsl:text> dispatch(value) { + <xsl:text> this.display_val = value; + <xsl:text> this.request_animate(); + <xsl:text> if(this.value_elt) + <xsl:text> this.value_elt.textContent = String(this.display_val); + <xsl:text> let [min,max,start,end] = this.range; + <xsl:text> let [cx,cy] = this.center; + <xsl:text> let [rx,ry] = this.proportions; + <xsl:text> let tip = start + (end-start)*Number(this.display_val)/(max-min); + <xsl:text> let size = 0; + <xsl:text> if (tip-start > Math.PI) + <xsl:text> this.path_elt.setAttribute('d', "M "+(cx+rx*Math.cos(start))+","+(cy+ry*Math.sin(start))+ + <xsl:text> " A "+rx+","+ry+ + <xsl:text> " 1 "+(cx+rx*Math.cos(tip))+","+(cy+ry*Math.sin(tip))); + <xsl:text> if(this.args.length >= 2) + <xsl:text> [this.min, this.max]=this.args; + <xsl:text> let [start, end, cx, cy, rx, ry] = ["start", "end", "cx", "cy", "rx", "ry"]. + <xsl:text> map(tag=>Number(this.path_elt.getAttribute('sodipodi:'+tag))) + <xsl:text> if (ry == 0) + <xsl:text> if (start > end) + <xsl:text> end = end + 2*Math.PI; + <xsl:text> let [min,max] = [[this.min_elt,0],[this.max_elt,100]].map(([elt,def],i)=>elt? + <xsl:text> Number(elt.textContent) : + <xsl:text> this.args.length >= i+1 ? this.args[i] : def); + <xsl:text> this.range = [min, max, start, end]; + <xsl:text> this.center = [cx, cy]; + <xsl:text> this.proportions = [rx, ry]; + <xsl:template match="widget[@type='CircularBar']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>path</xsl:text> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>min max</xsl:text> + <xsl:with-param name="mandatory" select="'no'"/> + <xsl:template match="widget[@type='CustomHtml']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>CustomHtml widget allows insertion of HTML code in a svg:foreignObject. + <xsl:text>Widget content is replaced by foreignObject. HTML code is obtained from + <xsl:text>"code" labeled text content. HTML insert position and size is given with + <xsl:text>"container" labeled element. + <xsl:text>Custom HTML insert</xsl:text> + <xsl:template match="widget[@type='CustomHtml']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>CustomHtmlWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 5; + <xsl:text> widget_size = undefined; + <xsl:text> dispatch(value) { + <xsl:text> this.request_animate(); + <xsl:text> this.widget_size = this.container_elt.getBBox(); + <xsl:text> this.element.innerHTML ='<foreignObject x="'+ + <xsl:text> this.widget_size.x+'" y="'+this.widget_size.y+ + <xsl:text> '" width="'+this.widget_size.width+'" height="'+this.widget_size.height+'"> '+ + <xsl:text> this.code_elt.textContent+ + <xsl:text> ' </foreignObject>'; + <xsl:template match="widget[@type='CustomHtml']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>container code</xsl:text> + <xsl:template match="widget[@type='Display']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>If Display widget is a svg:text element, then text content is replaced by + <xsl:text>value of given variables, space separated. + <xsl:text>Otherwise, if Display widget is a group containing a svg:text element + <xsl:text>labelled "format", then text content is replaced by printf-like formated + <xsl:text>string. In other words, if "format" labeled text is "%d %s %f", then 3 + <xsl:text>variables paths are expected : HMI_IN, HMI_STRING and HMI_REAL. + <xsl:text>In case Display widget is a svg::text element, it is also possible to give + <xsl:text>format string as first argument. + <xsl:text>Printf-like formated text display</xsl:text> + <arg name="format" count="optional" accepts="string"> + <xsl:text>printf-like format string when not given as svg:text</xsl:text> + <path name="fields" count="many" accepts="HMI_INT,HMI_REAL,HMI_STRING,HMI_BOOL"> + <xsl:text>variables to be displayed</xsl:text> + <xsl:template match="widget[@type='Display']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>DisplayWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 5; + <xsl:text> dispatch(value, oldval, index) { + <xsl:text> this.fields[index] = value; + <xsl:text> if(!this.ready){ + <xsl:text> this.readyfields[index] = true; + <xsl:text> this.ready = this.readyfields.every(x=>x); + <xsl:text> this.request_animate(); + <xsl:template match="widget[@type='Display']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:variable name="format"> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>format</xsl:text> + <xsl:with-param name="mandatory" select="'no'"/> + <xsl:variable name="has_format" select="string-length($format)>0"/> + <xsl:value-of select="$format"/> + <xsl:if test="$hmi_element[not(self::svg:text)] and not($has_format)"> + <xsl:message terminate="yes"> + <xsl:text>Display Widget id="</xsl:text> + <xsl:value-of select="$hmi_element/@id"/> + <xsl:text>" must be a svg::text element itself or a group containing a svg:text element labelled "format"</xsl:text> + <xsl:variable name="field_initializer"> + <xsl:for-each select="path"> + <xsl:when test="@type='HMI_STRING'"> + <xsl:text>""</xsl:text> + <xsl:if test="position()!=last()"> + <xsl:text> fields: [</xsl:text> + <xsl:value-of select="$field_initializer"/> + <xsl:variable name="readyfield_initializer"> + <xsl:for-each select="path"> + <xsl:text>false</xsl:text> + <xsl:if test="position()!=last()"> + <xsl:text> readyfields: [</xsl:text> + <xsl:value-of select="$readyfield_initializer"/> + <xsl:text> ready: false, + <xsl:text> animate: function(){ + <xsl:when test="$has_format"> + <xsl:text> if(this.format_elt.getAttribute("lang")) { + <xsl:text> this.format = svg_text_to_multiline(this.format_elt); + <xsl:text> this.format_elt.removeAttribute("lang"); + <xsl:text> let str = vsprintf(this.format,this.fields); + <xsl:text> multiline_to_svg_text(this.format_elt, str, !this.ready); + <xsl:text> let str = this.args.length == 1 ? vsprintf(this.args[0],this.fields) : this.fields.join(' '); + <xsl:text> multiline_to_svg_text(this.element, str, !this.ready); + <xsl:text> init: function() { + <xsl:if test="$has_format"> + <xsl:text> this.format = svg_text_to_multiline(this.format_elt); + <xsl:text> this.animate(); + <xsl:template match="widget[@type='DropDown']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>DropDown widget can have one, two or three path variables. + <xsl:text>It needs "text" (svg:text or svg:use referring to svg:text), + <xsl:text>"box" (svg:rect), "button" (svg:*), and "highlight" (svg:rect) + <xsl:text>labeled elements. + <xsl:text>When user clicks on "button", "text" is duplicated to display entries in the + <xsl:text>limit of available space in page, and "box" is extended to contain all + <xsl:text>texts. "highlight" is moved over pre-selected entry. + <xsl:text>The first variable path is index of selection, and the second is value of selection. + <xsl:text>In case there are one or two path variables, a list of texts is defined via + <xsl:text>If there are no arguments, it is expected that "text" labeled element is of + <xsl:text>type svg:use and refers to a svg:text element part of a TextList widget. + <xsl:text>In that case list of texts is set to TextList content. + <xsl:text>When only one argument is given and its value is "#langs" then list of + <xsl:text>texts is automatically set to the human-readable list of supported + <xsl:text>languages by this HMI. + <xsl:text>Otherwise, arguments are used as dropdown options. + <xsl:text>In case there are three path variables, arguments are not expected and ignored. + <xsl:text>The third path variable is a string containing the list of entries. + <xsl:text>HMI:DropDown:Red:Green:Blue:Other@/SELECTED_INDEX@/SELECTED_VALUE + <xsl:text>HMI:DropDown@/SELECTED_INDEX@/SELECTED_VALUE@/OPTIONS + <xsl:text>Let user select text entry in a drop-down menu</xsl:text> + <arg name="entries" count="many" accepts="string"> + <xsl:text>drop-down menu entries</xsl:text> + <path name="selected_inex" accepts="HMI_INT"> + <xsl:text>selection index</xsl:text> + <path name="selected_value" accepts="HMI_STRING"> + <xsl:text>selection value</xsl:text> + <path name="options" accepts="HMI_STRING"> + <xsl:text>drop-down menu entries</xsl:text> + <xsl:template match="widget[@type='DropDown']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>DropDownWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> dispatch(value, old_val, index) { + <xsl:text> if (index == 0) { + <xsl:text> if (!this.opened) this.set_selection(value); + <xsl:text> } else if (index == 2) { + <xsl:text> this.content = value.split(":"); + <xsl:text> this.init_specific(); + <xsl:text> this.button_elt.onclick = this.on_button_click.bind(this); + <xsl:text> // Save original size of rectangle + <xsl:text> this.box_bbox = this.box_elt.getBBox() + <xsl:text> this.highlight_bbox = this.highlight_elt.getBBox() + <xsl:text> this.highlight_elt.style.visibility = "hidden"; + <xsl:text> // Compute margins + <xsl:text> this.text_bbox = this.text_elt.getBBox(); + <xsl:text> let lmargin = this.text_bbox.x - this.box_bbox.x; + <xsl:text> let tmargin = this.text_bbox.y - this.box_bbox.y; + <xsl:text> this.margins = [lmargin, tmargin].map(x => Math.max(x,0)); + <xsl:text> // Index of first visible element in the menu, when opened + <xsl:text> this.menu_offset = 0; + <xsl:text> // How mutch to lift the menu vertically so that it does not cross bottom border + <xsl:text> this.lift = 0; + <xsl:text> // Event handlers cannot be object method ('this' is unknown) + <xsl:text> // as a workaround, handler given to addEventListener is bound in advance. + <xsl:text> this.bound_close_on_click_elsewhere = this.close_on_click_elsewhere.bind(this); + <xsl:text> this.bound_on_selection_click = this.on_selection_click.bind(this); + <xsl:text> this.bound_on_backward_click = this.on_backward_click.bind(this); + <xsl:text> this.bound_on_forward_click = this.on_forward_click.bind(this); + <xsl:text> this.opened = false; + <xsl:text> this.clickables = []; + <xsl:text> on_button_click() { + <xsl:text> this.open(); + <xsl:text> // Called when a menu entry is clicked + <xsl:text> on_selection_click(selection) { + <xsl:text> this.close(); + <xsl:text> this.apply_hmi_value(0, selection); + <xsl:text> on_backward_click(){ + <xsl:text> this.scroll(false); + <xsl:text> on_forward_click(){ + <xsl:text> this.scroll(true); + <xsl:text> set_selection(value) { + <xsl:text> let display_str; + <xsl:text> if(value >= 0 && value < this.content.length){ + <xsl:text> // if valid selection resolve content + <xsl:text> display_str = gettext(this.content[value]); + <xsl:text> this.last_selection = value; + <xsl:text> // otherwise show problem + <xsl:text> display_str = "?"+String(value)+"?"; + <xsl:text> // It is assumed that first span always stays, + <xsl:text> // and contains selection when menu is closed + <xsl:text> this.text_elt.firstElementChild.textContent = display_str; + <xsl:text> // If there is more than one path variable, + <xsl:text> // meaning there is a variable for selection value, + <xsl:text> // write the value in it + <xsl:text> if (this.indexes_length > 1) + <xsl:text> this.apply_hmi_value(1, display_str); + <xsl:text> grow_text(up_to) { + <xsl:text> let count = 1; + <xsl:text> let txt = this.text_elt; + <xsl:text> let first = txt.firstElementChild; + <xsl:text> // Real world (pixels) boundaries of current page + <xsl:text> let bounds = svg_root.getBoundingClientRect(); + <xsl:text> this.lift = 0; + <xsl:text> while(count < up_to) { + <xsl:text> let next = first.cloneNode(); + <xsl:text> // relative line by line text flow instead of absolute y coordinate + <xsl:text> next.removeAttribute("y"); + <xsl:text> next.setAttribute("dy", "1.1em"); + <xsl:text> // default content to allow computing text element bbox + <xsl:text> next.textContent = "..."; + <xsl:text> // append new span to text element + <xsl:text> txt.appendChild(next); + <xsl:text> // now check if text extended by one row fits to page + <xsl:text> // FIXME : exclude margins to be more accurate on box size + <xsl:text> let rect = txt.getBoundingClientRect(); + <xsl:text> if(rect.bottom > bounds.bottom){ + <xsl:text> // in case of overflow at the bottom, lift up one row + <xsl:text> let backup = first.getAttribute("dy"); + <xsl:text> // apply lift as a dy added too first span (y attrib stays) + <xsl:text> first.setAttribute("dy", "-"+String((this.lift+1)*1.1)+"em"); + <xsl:text> rect = txt.getBoundingClientRect(); + <xsl:text> if(rect.top > bounds.top){ + <xsl:text> this.lift += 1; + <xsl:text> // if it goes over the top, then backtrack + <xsl:text> // restore dy attribute on first span + <xsl:text> first.setAttribute("dy", backup); + <xsl:text> first.removeAttribute("dy"); + <xsl:text> // remove unwanted child + <xsl:text> txt.removeChild(next); + <xsl:text> return count; + <xsl:text> return count; + <xsl:text> close_on_click_elsewhere(e) { + <xsl:text> // inhibit events not targetting spans (menu items) + <xsl:text> if([this.text_elt, this.element].indexOf(e.target.parentNode) == -1){ + <xsl:text> e.stopPropagation(); + <xsl:text> // close menu in case click is outside box + <xsl:text> if(e.target !== this.box_elt) + <xsl:text> this.close(); + <xsl:text> // Stop hogging all click events + <xsl:text> svg_root.removeEventListener("pointerdown", this.numb_event, true); + <xsl:text> svg_root.removeEventListener("pointerup", this.numb_event, true); + <xsl:text> svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true); + <xsl:text> // Restore position and sixe of widget elements + <xsl:text> this.reset_text(); + <xsl:text> this.reset_clickables(); + <xsl:text> this.reset_box(); + <xsl:text> this.reset_highlight(); + <xsl:text> // Put the button back in place + <xsl:text> this.element.appendChild(this.button_elt); + <xsl:text> // Mark as closed (to allow dispatch) + <xsl:text> this.opened = false; + <xsl:text> // Dispatch last cached value + <xsl:text> this.apply_cache(); + <xsl:text> // Make item (text span) clickable by overlaying a rectangle on top of it + <xsl:text> make_clickable(span, func) { + <xsl:text> let txt = this.text_elt; + <xsl:text> let original_text_y = this.text_bbox.y; + <xsl:text> let highlight = this.highlight_elt; + <xsl:text> let original_h_y = this.highlight_bbox.y; + <xsl:text> let clickable = highlight.cloneNode(); + <xsl:text> let yoffset = span.getBBox().y - original_text_y; + <xsl:text> clickable.y.baseVal.value = original_h_y + yoffset; + <xsl:text> clickable.style.pointerEvents = "bounding-box"; + <xsl:text> //clickable.style.visibility = "hidden"; + <xsl:text> //clickable.onclick = () => alert("love JS"); + <xsl:text> clickable.onclick = func; + <xsl:text> this.element.appendChild(clickable); + <xsl:text> this.clickables.push(clickable) + <xsl:text> reset_clickables() { + <xsl:text> while(this.clickables.length){ + <xsl:text> this.element.removeChild(this.clickables.pop()); + <xsl:text> // Set text content when content is smaller than menu (no scrolling) + <xsl:text> set_complete_text(){ + <xsl:text> let spans = this.text_elt.children; + <xsl:text> for(let item of this.content){ + <xsl:text> let span=spans[c]; + <xsl:text> span.textContent = gettext(item); + <xsl:text> let sel = c; + <xsl:text> this.make_clickable(span, (evt) => this.bound_on_selection_click(sel)); + <xsl:text> // Move partial view : + <xsl:text> // false : upward, lower value + <xsl:text> // true : downward, higher value + <xsl:text> scroll(forward){ + <xsl:text> let contentlength = this.content.length; + <xsl:text> let spans = this.text_elt.children; + <xsl:text> let spanslength = spans.length; + <xsl:text> // reduce accounted menu size according to prsence of scroll buttons + <xsl:text> // since we scroll there is necessarly one button + <xsl:text> spanslength--; + <xsl:text> if(forward){ + <xsl:text> // reduce accounted menu size because of back button + <xsl:text> // in current view + <xsl:text> if(this.menu_offset > 0) spanslength--; + <xsl:text> this.menu_offset = Math.min( + <xsl:text> contentlength - spans.length + 1, + <xsl:text> this.menu_offset + spanslength); + <xsl:text> // reduce accounted menu size because of back button + <xsl:text> // in view once scrolled + <xsl:text> if(this.menu_offset - spanslength > 0) spanslength--; + <xsl:text> this.menu_offset = Math.max( + <xsl:text> this.menu_offset - spanslength); + <xsl:text> if(this.menu_offset == 1) + <xsl:text> this.menu_offset = 0; + <xsl:text> this.reset_highlight(); + <xsl:text> this.reset_clickables(); + <xsl:text> this.set_partial_text(); + <xsl:text> this.highlight_selection(); + <xsl:text> // Setup partial view text content + <xsl:text> // with jumps at first and last entry when appropriate + <xsl:text> set_partial_text(){ + <xsl:text> let spans = this.text_elt.children; + <xsl:text> let contentlength = this.content.length; + <xsl:text> let spanslength = spans.length; + <xsl:text> let i = this.menu_offset, c = 0; + <xsl:text> let m = this.box_bbox; + <xsl:text> while(c < spanslength){ + <xsl:text> let span=spans[c]; + <xsl:text> let onclickfunc; + <xsl:text> // backward jump only present if not exactly at start + <xsl:text> if(c == 0 && i != 0){ + <xsl:text> span.textContent = "▲"; + <xsl:text> onclickfunc = this.bound_on_backward_click; + <xsl:text> let o = span.getBBox(); + <xsl:text> span.setAttribute("dx", (m.width - o.width)/2); + <xsl:text> // presence of forward jump when not right at the end + <xsl:text> }else if(c == spanslength-1 && i < contentlength - 1){ + <xsl:text> span.textContent = "▼"; + <xsl:text> onclickfunc = this.bound_on_forward_click; + <xsl:text> let o = span.getBBox(); + <xsl:text> span.setAttribute("dx", (m.width - o.width)/2); + <xsl:text> // otherwise normal content + <xsl:text> span.textContent = gettext(this.content[i]); + <xsl:text> let sel = i; + <xsl:text> onclickfunc = (evt) => this.bound_on_selection_click(sel); + <xsl:text> span.removeAttribute("dx"); + <xsl:text> this.make_clickable(span, onclickfunc); + <xsl:text> numb_event(e) { + <xsl:text> e.stopPropagation(); + <xsl:text> let length = this.content.length; + <xsl:text> // systematically reset text, to strip eventual whitespace spans + <xsl:text> this.reset_text(); + <xsl:text> // grow as much as needed or possible + <xsl:text> let slots = this.grow_text(length); + <xsl:text> // Depending on final size + <xsl:text> if(slots == length) { + <xsl:text> // show all at once + <xsl:text> this.set_complete_text(); + <xsl:text> // eventualy align menu to current selection, compensating for lift + <xsl:text> let offset = this.last_selection - this.lift; + <xsl:text> if(offset > 0) + <xsl:text> this.menu_offset = Math.min(offset + 1, length - slots + 1); + <xsl:text> this.menu_offset = 0; + <xsl:text> // show surrounding values + <xsl:text> this.set_partial_text(); + <xsl:text> // Now that text size is known, we can set the box around it + <xsl:text> this.adjust_box_to_text(); + <xsl:text> // Take button out until menu closed + <xsl:text> this.element.removeChild(this.button_elt); + <xsl:text> // Rise widget to top by moving it to last position among siblings + <xsl:text> this.element.parentNode.appendChild(this.element.parentNode.removeChild(this.element)); + <xsl:text> // disable interaction with background + <xsl:text> svg_root.addEventListener("pointerdown", this.numb_event, true); + <xsl:text> svg_root.addEventListener("pointerup", this.numb_event, true); + <xsl:text> svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true); + <xsl:text> this.highlight_selection(); + <xsl:text> // mark as open + <xsl:text> this.opened = true; + <xsl:text> // Put text element in normalized state + <xsl:text> reset_text(){ + <xsl:text> let txt = this.text_elt; + <xsl:text> let first = txt.firstElementChild; + <xsl:text> // remove attribute eventually added to first text line while opening + <xsl:text> first.onclick = null; + <xsl:text> first.removeAttribute("dy"); + <xsl:text> first.removeAttribute("dx"); + <xsl:text> // keep only the first line of text + <xsl:text> for(let span of Array.from(txt.children).slice(1)){ + <xsl:text> txt.removeChild(span) + <xsl:text> // Put rectangle element in saved original state + <xsl:text> reset_box(){ + <xsl:text> let m = this.box_bbox; + <xsl:text> let b = this.box_elt; + <xsl:text> b.x.baseVal.value = m.x; + <xsl:text> b.y.baseVal.value = m.y; + <xsl:text> b.width.baseVal.value = m.width; + <xsl:text> b.height.baseVal.value = m.height; + <xsl:text> highlight_selection(){ + <xsl:text> if(this.last_selection == undefined) return; + <xsl:text> let highlighted_row = this.last_selection - this.menu_offset; + <xsl:text> if(highlighted_row < 0) return; + <xsl:text> let spans = this.text_elt.children; + <xsl:text> let spanslength = spans.length; + <xsl:text> let contentlength = this.content.length; + <xsl:text> if(this.menu_offset != 0) { + <xsl:text> spanslength--; + <xsl:text> highlighted_row++; + <xsl:text> if(this.menu_offset + spanslength < contentlength - 1) spanslength--; + <xsl:text> if(highlighted_row > spanslength) return; + <xsl:text> let original_text_y = this.text_bbox.y; + <xsl:text> let highlight = this.highlight_elt; + <xsl:text> let span = spans[highlighted_row]; + <xsl:text> let yoffset = span.getBBox().y - original_text_y; + <xsl:text> highlight.y.baseVal.value = this.highlight_bbox.y + yoffset; + <xsl:text> highlight.style.visibility = "visible"; + <xsl:text> reset_highlight(){ + <xsl:text> let highlight = this.highlight_elt; + <xsl:text> highlight.y.baseVal.value = this.highlight_bbox.y; + <xsl:text> highlight.style.visibility = "hidden"; + <xsl:text> // Use margin and text size to compute box size + <xsl:text> adjust_box_to_text(){ + <xsl:text> let [lmargin, tmargin] = this.margins; + <xsl:text> let m = this.text_elt.getBBox(); + <xsl:text> let b = this.box_elt; + <xsl:text> // b.x.baseVal.value = m.x - lmargin; + <xsl:text> b.y.baseVal.value = m.y - tmargin; + <xsl:text> // b.width.baseVal.value = 2 * lmargin + m.width; + <xsl:text> b.height.baseVal.value = 2 * tmargin + m.height; + <xsl:template match="widget[@type='DropDown']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>box button highlight</xsl:text> + <xsl:variable name="text_elt" select="$hmi_element//*[@inkscape:label='text'][1]"/> + <xsl:text>init_specific: function() { + <xsl:when test="count(path) = 3"> + <xsl:text> this.text_elt = id("</xsl:text> + <xsl:value-of select="$text_elt/@id"/> + <xsl:when test="count(arg) = 1 and arg[1]/@value = '#langs'"> + <xsl:text> this.text_elt = id("</xsl:text> + <xsl:value-of select="$text_elt/@id"/> + <xsl:text> this.content = langs.map(([lname,lcode]) => lname); + <xsl:when test="count(arg) = 0"> + <xsl:if test="not($text_elt[self::svg:use])"> + <xsl:message terminate="yes"> + <xsl:text>No argument for HMI:DropDown widget id="</xsl:text> + <xsl:value-of select="$hmi_element/@id"/> + <xsl:text>" and "text" labeled element is not a svg:use element</xsl:text> + <xsl:variable name="real_text_elt" select="$result_widgets[@id = $hmi_element/@id]//*[@original=$text_elt/@id]/svg:text"/> + <xsl:text> this.text_elt = id("</xsl:text> + <xsl:value-of select="$real_text_elt/@id"/> + <xsl:variable name="from_list_id" select="substring-after($text_elt/@xlink:href,'#')"/> + <xsl:variable name="from_list" select="$hmi_textlists[(@id | */@id) = $from_list_id]"/> + <xsl:if test="count($from_list) = 0"> + <xsl:message terminate="yes"> + <xsl:text>HMI:DropDown widget id="</xsl:text> + <xsl:value-of select="$hmi_element/@id"/> + <xsl:text>" "text" labeled element does not point to a svg:text owned by a HMI:List widget</xsl:text> + <xsl:text> this.content = hmi_widgets["</xsl:text> + <xsl:value-of select="$from_list/@id"/> + <xsl:text> this.text_elt = id("</xsl:text> + <xsl:value-of select="$text_elt/@id"/> + <xsl:text> this.content = [ + <xsl:for-each select="arg"> + <xsl:value-of select="@value"/> + <declarations:DropDown/> + <xsl:template match="declarations:DropDown"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:text>function gettext(o) { + <xsl:text> if(typeof(o) == "string"){ + <xsl:text> return svg_text_to_multiline(o); + <xsl:template match="widget[@type='ForEach']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>ForEach widget is used to span a small set of widget over a larger set of + <xsl:text>repeated HMI_NODEs. + <xsl:text>Idea is somewhat similar to relative page, but it all happens inside the + <xsl:text>ForEach widget, no page involved. + <xsl:text>Together with relative Jump widgets it can be used to build a menu to reach + <xsl:text>relative pages covering many identical HMI_NODES siblings. + <xsl:text>ForEach widget takes a HMI_CLASS name as argument and a HMI_NODE path as + <xsl:text>Direct sub-elements can be either groups of widget to be spanned, labeled + <xsl:text>"ClassName:offset", or buttons to control the spanning, labeled + <xsl:text>"ClassName:+/-number". + <xsl:text>In case of "ClassName:offset", offset for first element is 1. + <xsl:text>span widgets over a set of repeated HMI_NODEs</xsl:text> + <arg name="class_name" accepts="string"> + <xsl:text>HMI_CLASS name</xsl:text> + <path name="root" accepts="HMI_NODE"> + <xsl:text> where to find HMI_NODEs whose HMI_CLASS is class_name</xsl:text> + <path name="position" accepts="HMI_INT"> + <xsl:text>position of HMI_NODE mapped to first item, among similar siblings</xsl:text> + <path name="range" accepts="HMI_INT" count="optional"> + <xsl:text> count of HMI_NODE siblings</xsl:text> + <path name="size" accepts="HMI_INT" count="optional"> + <xsl:text> count of visible items</xsl:text> + <xsl:template match="widget[@type='ForEach']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:if test="count(path) < 1"> + <xsl:message terminate="yes"> + <xsl:text>ForEach widget </xsl:text> + <xsl:value-of select="$hmi_element/@id"/> + <xsl:text> must have one HMI path given.</xsl:text> + <xsl:if test="count(arg) != 1"> + <xsl:message terminate="yes"> + <xsl:text>ForEach widget </xsl:text> + <xsl:value-of select="$hmi_element/@id"/> + <xsl:text> must have one argument given : a class name.</xsl:text> + <xsl:variable name="class" select="arg[1]/@value"/> + <xsl:variable name="base_path" select="path/@value"/> + <xsl:variable name="hmi_index_base" select="$indexed_hmitree/*[@hmipath = $base_path]"/> + <xsl:variable name="hmi_tree_base" select="$hmitree/descendant-or-self::*[@path = $hmi_index_base/@path]"/> + <xsl:variable name="hmi_tree_items" select="$hmi_tree_base/*[@class = $class]"/> + <xsl:variable name="hmi_index_items" select="$indexed_hmitree/*[@path = $hmi_tree_items/@path]"/> + <xsl:variable name="items_paths" select="$hmi_index_items/@hmipath"/> + <xsl:text> index_pool: [ + <xsl:for-each select="$hmi_index_items"> + <xsl:value-of select="@index"/> + <xsl:if test="position()!=last()"> + <xsl:text> init: function() { + <xsl:variable name="prefix" select="concat($class,':')"/> + <xsl:variable name="buttons_regex" select="concat('^',$prefix,'[+\-][0-9]+')"/> + <xsl:variable name="buttons" select="$hmi_element/*[regexp:test(@inkscape:label, $buttons_regex)]"/> + <xsl:for-each select="$buttons"> + <xsl:variable name="op" select="substring-after(@inkscape:label, $prefix)"/> + <xsl:text> id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>").setAttribute("onclick", "hmi_widgets['</xsl:text> + <xsl:value-of select="$hmi_element/@id"/> + <xsl:text>'].on_click('</xsl:text> + <xsl:value-of select="$op"/> + <xsl:text> this.items = [ + <xsl:variable name="items_regex" select="concat('^',$prefix,'[0-9]+')"/> + <xsl:variable name="unordered_items" select="$hmi_element//*[regexp:test(@inkscape:label, $items_regex)]"/> + <xsl:for-each select="$unordered_items"> + <xsl:variable name="elt_label" select="concat($prefix, string(position()))"/> + <xsl:variable name="elt" select="$unordered_items[@inkscape:label = $elt_label]"/> + <xsl:variable name="pos" select="position()"/> + <xsl:variable name="item_path" select="$items_paths[$pos]"/> + <xsl:text> [ /* item="</xsl:text> + <xsl:value-of select="$elt_label"/> + <xsl:text>" path="</xsl:text> + <xsl:value-of select="$item_path"/> + <xsl:if test="count($elt)=0"> + <xsl:message terminate="yes"> + <xsl:text>Missing item labeled </xsl:text> + <xsl:value-of select="$elt_label"/> + <xsl:text> in ForEach widget </xsl:text> + <xsl:value-of select="$hmi_element/@id"/> + <xsl:if test="count($elt)>1"> + <xsl:message terminate="yes"> + <xsl:text>Duplicate item labeled </xsl:text> + <xsl:value-of select="$elt_label"/> + <xsl:text> in ForEach widget </xsl:text> + <xsl:value-of select="$hmi_element/@id"/> + <xsl:for-each select="func:refered_elements($elt)[@id = $hmi_elements/@id][not(@id = $elt/@id)]"> + <xsl:if test="not(func:is_descendant_path(func:widget(@id)/path/@value, $item_path))"> + <xsl:message terminate="yes"> + <xsl:text>Widget id="</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>" label="</xsl:text> + <xsl:value-of select="@inkscape:label"/> + <xsl:text>" is having wrong path. Accroding to ForEach widget ancestor id="</xsl:text> + <xsl:value-of select="$hmi_element/@id"/> + <xsl:text>", path should be descendant of "</xsl:text> + <xsl:value-of select="$item_path"/> + <xsl:text>".</xsl:text> + <xsl:text> hmi_widgets["</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>"]</xsl:text> + <xsl:if test="position()!=last()"> + <xsl:text> ]</xsl:text> + <xsl:if test="position()!=last()"> + <xsl:text> range: </xsl:text> + <xsl:value-of select="count($hmi_index_items)"/> + <xsl:text> size: </xsl:text> + <xsl:value-of select="count($unordered_items)"/> + <xsl:text> position: 0, + <xsl:template match="widget[@type='ForEach']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>ForEachWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> items_subscribed = false; + <xsl:text> unsub_items(){ + <xsl:text> if(this.items_subscribed){ + <xsl:text> for(let item of this.items){ + <xsl:text> for(let widget of item) { + <xsl:text> widget.unsub(); + <xsl:text> this.items_subscribed = false; + <xsl:text> super.unsub() + <xsl:text> this.unsub_items(); + <xsl:text> sub_items(){ + <xsl:text> if(!this.items_subscribed){ + <xsl:text> for(let i = 0; i < this.size; i++) { + <xsl:text> let item = this.items[i]; + <xsl:text> let orig_item_index = this.index_pool[i]; + <xsl:text> let item_index = this.index_pool[i+this.position]; + <xsl:text> let item_index_offset = item_index - orig_item_index; + <xsl:text> if(this.relativeness[0]) + <xsl:text> item_index_offset += this.offset; + <xsl:text> for(let widget of item) { + <xsl:text> /* all variables of all widgets in a ForEach are all relative. + <xsl:text> TODO: allow absolute variables in ForEach widgets + <xsl:text> widget.sub(item_index_offset, widget.indexes.map(_=>true)); + <xsl:text> sub(new_offset, relativeness, container_id){ + <xsl:text> let position_given = this.indexes.length > 1; + <xsl:text> // sub() will call apply_cache() and then dispatch() + <xsl:text> // undefining position forces dispatch() to call apply_position() + <xsl:text> if(position_given) + <xsl:text> this.position = undefined; + <xsl:text> super.sub(new_offset, relativeness, container_id); + <xsl:text> // if position isn't given as a variable + <xsl:text> // dispatch() to call apply_position() aren't called + <xsl:text> // and items must be subscibed now. + <xsl:text> if(!position_given) + <xsl:text> this.sub_items(); + <xsl:text> // as soon as subribed apply range and size once for all + <xsl:text> if(this.indexes.length > 2) + <xsl:text> this.apply_hmi_value(2, this.range); + <xsl:text> if(this.indexes.length > 3) + <xsl:text> this.apply_hmi_value(3, this.size); + <xsl:text> apply_position(new_position){ + <xsl:text> let old_position = this.position; + <xsl:text> let limited_position = Math.round(Math.max(Math.min(new_position, this.range - this.size), 0)); + <xsl:text> if(this.position == limited_position){ + <xsl:text> return false; + <xsl:text> this.unsub_items(); + <xsl:text> this.position = limited_position; + <xsl:text> this.sub_items(); + <xsl:text> request_subscriptions_update(); + <xsl:text> jumps_need_update = true; + <xsl:text> this.request_animate(); + <xsl:text> return true; + <xsl:text> on_click(opstr, evt) { + <xsl:text> let new_position = eval(String(this.position)+opstr); + <xsl:text> if(new_position + this.size > this.range) { + <xsl:text> if(this.position + this.size == this.range) + <xsl:text> new_position = 0; + <xsl:text> new_position = this.range - this.size; + <xsl:text> } else if(new_position < 0) { + <xsl:text> if(this.position == 0) + <xsl:text> new_position = this.range - this.size; + <xsl:text> new_position = 0; + <xsl:text> if(this.apply_position(new_position)){ + <xsl:text> this.apply_hmi_value(1, this.position); + <xsl:text> dispatch(value, oldval, index) { + <xsl:text> // Only care about position, others are constants + <xsl:text> if(index == 1){ + <xsl:text> this.apply_position(value); + <xsl:text> if(this.position != value){ + <xsl:text> // widget refused or apply different value, force it back + <xsl:text> this.apply_hmi_value(1, this.position); + <xsl:template match="widget[@type='Image']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>If Image widget is a svg:image element, then href content is replaced by + <xsl:text>value of given variable. + <xsl:text>Image display</xsl:text> + <xsl:template match="widget[@type='Image']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>ImageWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 5; + <xsl:text> dispatch(value, oldval, index) { + <xsl:text> if (index == 0) { + <xsl:text> this.given_url = value; + <xsl:text> this.ready = true; + <xsl:text> this.request_animate(); + <xsl:template match="widget[@type='Image']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:text> given_url: "", + <xsl:text> ready: false, + <xsl:text> animate: function(){ + <xsl:text> this.element.setAttribute('href', this.given_url); + <xsl:template xmlns="http://www.w3.org/2000/svg" mode="inline_svg" match="svg:image[starts-with(@inkscape:label, 'HMI:Image')]"> + <xsl:apply-templates mode="inline_svg" select="@*[not(contains(name(), 'href'))] | node()"/> + <xsl:template match="widget[@type='Input']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Input widget takes one variable path, and displays current value in + <xsl:text>optional "value" labeled sub-element. + <xsl:text>Click on optional "edit" labeled element opens keypad to edit value. + <xsl:text>Operation on current value is performed when click on sub-elements with + <xsl:text>label starting with '=', '+' or '-' sign. Value after sign is used as + <xsl:text>Input field with predefined operation buttons</xsl:text> + <arg name="format" accepts="string"> + <xsl:text>optional printf-like format </xsl:text> + <path name="edit" accepts="HMI_INT, HMI_REAL, HMI_STRING"> + <xsl:text>single variable to edit</xsl:text> + <xsl:template match="widget[@type='Input']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>InputWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> edit_callback(new_val) { + <xsl:text> this.apply_hmi_value(0, new_val); + <xsl:text> is_inhibited = false; + <xsl:text> this.is_inhibited = true; + <xsl:text> this.display = msg; + <xsl:text> setTimeout(() => this.stopalert(), 1000); + <xsl:text> this.request_animate(); + <xsl:text> stopalert(){ + <xsl:text> this.is_inhibited = false; + <xsl:text> this.display = this.last_value; + <xsl:text> this.request_animate(); + <xsl:text> overshot(new_val, max) { + <xsl:text> this.alert("max"); + <xsl:text> undershot(new_val, min) { + <xsl:text> this.alert("min"); + <xsl:text> display = ""; + <xsl:template match="widget[@type='Input']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:variable name="value_elt"> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>value</xsl:text> + <xsl:with-param name="mandatory" select="'no'"/> + <xsl:variable name="have_value" select="string-length($value_elt)>0"/> + <xsl:value-of select="$value_elt"/> + <xsl:variable name="edit_elt"> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>edit</xsl:text> + <xsl:with-param name="mandatory" select="'no'"/> + <xsl:variable name="have_edit" select="string-length($edit_elt)>0"/> + <xsl:value-of select="$edit_elt"/> + <xsl:variable name="action_elements" select="$hmi_element/*[regexp:test(@inkscape:label,'^[=+\-].+')]"/> + <xsl:if test="$have_value"> + <xsl:text> frequency: 5, + <xsl:text> dispatch: function(value, oldval, varnum) { + <xsl:text> if(varnum != 0) return; + <xsl:if test="$have_value or $have_edit"> + <xsl:when test="count(arg) = 1"> + <xsl:text> this.last_value = vsprintf("</xsl:text> + <xsl:value-of select="arg[1]/@value"/> + <xsl:text> this.last_value = value; + <xsl:text> if(!this.is_inhibited){ + <xsl:text> this.display = this.last_value; + <xsl:if test="$have_value"> + <xsl:text> this.request_animate(); + <xsl:if test="$have_value"> + <xsl:text> animate: function(){ + <xsl:text> multiline_to_svg_text(this.value_elt, String(this.display)); + <xsl:for-each select="$action_elements"> + <xsl:text> action_elt_</xsl:text> + <xsl:value-of select="position()"/> + <xsl:text>: id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:variable name="current_id" select="@id"/> + <xsl:variable name="active" select="$hmi_element/*[@id = $current_id]/*[regexp:test(@inkscape:label,'active')]"/> + <xsl:text> activable_sub_</xsl:text> + <xsl:value-of select="position()"/> + <xsl:for-each select="$active"> + <xsl:value-of select="@inkscape:label"/> + <xsl:text>_elt: id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>")</xsl:text> + <xsl:if test="position()!=last()"> + <xsl:text> on_op_mouse_down_</xsl:text> + <xsl:value-of select="position()"/> + <xsl:text>: function(){ + <xsl:text> svg_root.addEventListener("pointerup", this.bound_on_op_mouse_up_</xsl:text> + <xsl:value-of select="position()"/> + <xsl:text> set_activity_state(this.activable_sub_</xsl:text> + <xsl:value-of select="position()"/> + <xsl:text> on_op_mouse_up_</xsl:text> + <xsl:value-of select="position()"/> + <xsl:text>: function(){ + <xsl:text> svg_root.removeEventListener("pointerup", this.bound_on_op_mouse_up_</xsl:text> + <xsl:value-of select="position()"/> + <xsl:text> set_activity_state(this.activable_sub_</xsl:text> + <xsl:value-of select="position()"/> + <xsl:text> this.change_hmi_value(0, "</xsl:text> + <xsl:value-of select="func:escape_quotes(@inkscape:label)"/> + <xsl:text> init: function() { + <xsl:if test="$have_edit"> + <xsl:text> this.edit_elt.onclick = () => edit_value("</xsl:text> + <xsl:value-of select="path/@value"/> + <xsl:text>", "</xsl:text> + <xsl:value-of select="path/@type"/> + <xsl:text>", this, this.last_value); + <xsl:if test="$have_value"> + <xsl:text> this.value_elt.style.pointerEvents = "none"; + <xsl:text> this.animate(); + <xsl:for-each select="$action_elements"> + <xsl:text> this.action_elt_</xsl:text> + <xsl:value-of select="position()"/> + <xsl:text>.onmousedown = () => this.on_op_mouse_down_</xsl:text> + <xsl:value-of select="position()"/> + <xsl:text> this.bound_on_op_mouse_up_</xsl:text> + <xsl:value-of select="position()"/> + <xsl:text> = this.on_op_mouse_up_</xsl:text> + <xsl:value-of select="position()"/> + <xsl:if test="$have_value"> + <xsl:text> multiline_to_svg_text(this.value_elt, ""); + <xsl:template match="widget[@type='JsonTable']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Send given variables as POST to http URL argument, spread returned JSON in + <xsl:text>SVG sub-elements of "data" labeled element. + <xsl:text>Documentation to be written. see svghmi example. + <xsl:text>Http POST variables, spread JSON back</xsl:text> + <arg name="url" accepts="string"> + <path name="edit" accepts="HMI_INT, HMI_REAL, HMI_STRING"> + <xsl:text>single variable to edit</xsl:text> + <xsl:template match="widget[@type='JsonTable']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>JsonTableWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> // arbitrary defaults to avoid missing entries in query + <xsl:text> cache = [0,0,0]; + <xsl:text> init_common() { + <xsl:text> this.spread_json_data_bound = this.spread_json_data.bind(this); + <xsl:text> this.handle_http_response_bound = this.handle_http_response.bind(this); + <xsl:text> this.fetch_error_bound = this.fetch_error.bind(this); + <xsl:text> this.promised = false; + <xsl:text> handle_http_response(response) { + <xsl:text> if (!response.ok) { + <xsl:text> console.log("HTTP error, status = " + response.status); + <xsl:text> return response.json(); + <xsl:text> fetch_error(e){ + <xsl:text> console.log("HTTP fetch error, message = " + e.message + "Widget:" + this.element_id); + <xsl:text> do_http_request(...opt) { + <xsl:text> this.abort_controller = new AbortController(); + <xsl:text> return Promise.resolve().then(() => { + <xsl:text> const query = { + <xsl:text> args: this.args, + <xsl:text> range: this.cache[1], + <xsl:text> position: this.cache[2], + <xsl:text> visible: this.visible, + <xsl:text> extra: this.cache.slice(4), + <xsl:text> options: opt + <xsl:text> const options = { + <xsl:text> method: 'POST', + <xsl:text> body: JSON.stringify(query), + <xsl:text> headers: {'Content-Type': 'application/json'}, + <xsl:text> signal: this.abort_controller.signal + <xsl:text> return fetch(this.args[0], options) + <xsl:text> .then(this.handle_http_response_bound) + <xsl:text> .then(this.spread_json_data_bound) + <xsl:text> .catch(this.fetch_error_bound); + <xsl:text> this.abort_controller.abort(); + <xsl:text> super.unsub(); + <xsl:text> sub(...args){ + <xsl:text> this.cache[0] = undefined; + <xsl:text> super.sub(...args); + <xsl:text> dispatch(value, oldval, index) { + <xsl:text> if(this.cache[index] != value) + <xsl:text> this.cache[index] = value; + <xsl:text> if(!this.promised){ + <xsl:text> this.promised = true; + <xsl:text> this.do_http_request().finally(() => { + <xsl:text> this.promised = false; + <xsl:text> make_on_click(...options){ + <xsl:text> let that = this; + <xsl:text> return function(evt){ + <xsl:text> that.do_http_request(...options); + <xsl:text> // on_click(evt, ...options) { + <xsl:text> // this.do_http_request(...options); + <xsl:template mode="json_table_elt_render" match="svg:*"> + <xsl:message terminate="yes"> + <xsl:text>JsonTable Widget can't contain element of type </xsl:text> + <xsl:value-of select="local-name()"/> + <func:function name="func:json_expressions"> + <xsl:param name="expressions"/> + <xsl:param name="label"/> + <xsl:when test="$label"> + <xsl:variable name="suffixes" select="str:split($label)"/> + <xsl:variable name="res"> + <xsl:for-each select="$suffixes"> + <xsl:variable name="suffix" select="."/> + <xsl:variable name="pos" select="position()"/> + <xsl:variable name="expr" select="$expressions[position() <= $pos][last()]/expression"/> + <xsl:when test="contains($suffix,'=')"> + <xsl:variable name="name" select="substring-before($suffix,'=')"/> + <xsl:if test="$expr/@name[. != $name]"> + <xsl:message terminate="yes"> + <xsl:text>JsonTable : misplaced '=' or inconsistent names in Json data expressions.</xsl:text> + <xsl:attribute name="name"> + <xsl:value-of select="$name"/> + <xsl:attribute name="content"> + <xsl:value-of select="$expr/@content"/> + <xsl:value-of select="substring-after($suffix,'=')"/> + <xsl:copy-of select="$expr/@name"/> + <xsl:attribute name="content"> + <xsl:value-of select="$expr/@content"/> + <xsl:value-of select="$suffix"/> + <func:result select="exsl:node-set($res)"/> + <func:result select="$expressions"/> + <xsl:variable name="initexpr"> + <xsl:attribute name="content"> + <xsl:text>jdata</xsl:text> + <xsl:variable name="initexpr_ns" select="exsl:node-set($initexpr)"/> + <xsl:template mode="json_table_elt_render" match="svg:use"> + <xsl:param name="expressions"/> + <xsl:variable name="targetid" select="substring-after(@xlink:href,'#')"/> + <xsl:variable name="from_list" select="$hmi_lists[(@id | */@id) = $targetid]"/> + <xsl:when test="count($from_list) > 0"> + <xsl:text> id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>").href.baseVal = + <xsl:text> "#"+hmi_widgets["</xsl:text> + <xsl:value-of select="$from_list/@id"/> + <xsl:text>"].items[</xsl:text> + <xsl:value-of select="$expressions/expression[1]/@content"/> + <xsl:message terminate="no"> + <xsl:text>Clones (svg:use) in JsonTable Widget must point to a valid HMI:List widget or item. Reference "</xsl:text> + <xsl:value-of select="@xlink:href"/> + <xsl:text>" is not valid and will not be updated.</xsl:text> + <xsl:template mode="json_table_elt_render" match="svg:text"> + <xsl:param name="expressions"/> + <xsl:variable name="value_expr" select="$expressions/expression[1]/@content"/> + <xsl:variable name="original" select="@original"/> + <xsl:variable name="from_textstylelist" select="$textstylelist_related_ns/list[elt/@eltid = $original]"/> + <xsl:when test="count($from_textstylelist) > 0"> + <xsl:variable name="content_expr" select="$expressions/expression[2]/@content"/> + <xsl:if test="string-length($content_expr) = 0 or $expressions/expression[2]/@name != 'textContent'"> + <xsl:message terminate="yes"> + <xsl:text>Clones (svg:use) in JsonTable Widget pointing to a HMI:TextStyleList widget or item must have a "textContent=.someVal" assignment following value expression in label.</xsl:text> + <xsl:text> let elt = id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text> elt.textContent = String(</xsl:text> + <xsl:value-of select="$content_expr"/> + <xsl:text> elt.style = hmi_widgets["</xsl:text> + <xsl:value-of select="$from_textstylelist/@listid"/> + <xsl:text>"].styles[</xsl:text> + <xsl:value-of select="$value_expr"/> + <xsl:text> id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>").textContent = String(</xsl:text> + <xsl:value-of select="$value_expr"/> + <xsl:template mode="json_table_elt_render" match="svg:image"> + <xsl:param name="expressions"/> + <xsl:variable name="value_expr" select="$expressions/expression[1]/@content"/> + <xsl:text> id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>").setAttribute('href', String(</xsl:text> + <xsl:value-of select="$value_expr"/> + <func:function name="func:filter_non_widget_label"> + <xsl:param name="elt"/> + <xsl:param name="widget_elts"/> + <xsl:variable name="eltid"> + <xsl:when test="$elt/@original"> + <xsl:value-of select="$elt/@original"/> + <xsl:value-of select="$elt/@id"/> + <func:result select="$widget_elts[@id=$eltid]/@inkscape:label"/> + <xsl:template mode="json_table_render_except_comments" match="svg:*"> + <xsl:param name="expressions"/> + <xsl:param name="widget_elts"/> + <xsl:variable name="label" select="func:filter_non_widget_label(., $widget_elts)"/> + <xsl:if test="not(starts-with($label,'#'))"> + <xsl:apply-templates mode="json_table_render" select="."> + <xsl:with-param name="expressions" select="$expressions"/> + <xsl:with-param name="widget_elts" select="$widget_elts"/> + <xsl:with-param name="label" select="$label"/> + <xsl:template mode="json_table_render" match="svg:*"> + <xsl:param name="expressions"/> + <xsl:param name="widget_elts"/> + <xsl:param name="label"/> + <xsl:variable name="new_expressions" select="func:json_expressions($expressions, $label)"/> + <xsl:variable name="elt" select="."/> + <xsl:for-each select="$new_expressions/expression[position() > 1][starts-with(@name,'onClick')]"> + <xsl:text> id("</xsl:text> + <xsl:value-of select="$elt/@id"/> + <xsl:text>").onclick = this.make_on_click('</xsl:text> + <xsl:value-of select="@name"/> + <xsl:text>', </xsl:text> + <xsl:value-of select="@content"/> + <xsl:apply-templates mode="json_table_elt_render" select="."> + <xsl:with-param name="expressions" select="$new_expressions"/> + <xsl:template mode="json_table_render" match="svg:g"> + <xsl:param name="expressions"/> + <xsl:param name="widget_elts"/> + <xsl:param name="label"/> + <xsl:variable name="varprefix"> + <xsl:text>obj_</xsl:text> + <xsl:value-of select="@id"/> + <xsl:for-each select="$expressions/expression"> + <xsl:text> let </xsl:text> + <xsl:value-of select="$varprefix"/> + <xsl:value-of select="position()"/> + <xsl:text> = </xsl:text> + <xsl:value-of select="@content"/> + <xsl:text> if(</xsl:text> + <xsl:value-of select="$varprefix"/> + <xsl:value-of select="position()"/> + <xsl:text> == undefined) { + <xsl:variable name="new_expressions"> + <xsl:for-each select="$expressions/expression"> + <xsl:copy-of select="@name"/> + <xsl:attribute name="content"> + <xsl:value-of select="$varprefix"/> + <xsl:value-of select="position()"/> + <xsl:text> id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>").style = "</xsl:text> + <xsl:value-of select="@style"/> + <xsl:apply-templates mode="json_table_render_except_comments" select="*"> + <xsl:with-param name="expressions" select="func:json_expressions(exsl:node-set($new_expressions), $label)"/> + <xsl:with-param name="widget_elts" select="$widget_elts"/> + <xsl:text> } catch(err) { + <xsl:text> id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>").style = "display:none"; + <xsl:template match="widget[@type='JsonTable']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>data</xsl:text> + <xsl:variable name="data_elt" select="$result_svg_ns//*[@id = $hmi_element/@id]/*[@inkscape:label = 'data']"/> + <xsl:text> visible: </xsl:text> + <xsl:value-of select="count($data_elt/*[@inkscape:label])"/> + <xsl:text> spread_json_data: function(janswer) { + <xsl:text> let [range,position,jdata] = janswer; + <xsl:text> [[1, range], [2, position], [3, this.visible]].map(([i,v]) => { + <xsl:text> this.apply_hmi_value(i,v); + <xsl:text> this.cache[i] = v; + <xsl:apply-templates mode="json_table_render_except_comments" select="$data_elt"> + <xsl:with-param name="expressions" select="$initexpr_ns"/> + <xsl:with-param name="widget_elts" select="$hmi_element/*[@inkscape:label = 'data']/descendant::svg:*"/> + <xsl:text> this.init_common(); + <xsl:for-each select="$hmi_element/*[starts-with(@inkscape:label,'action_')]"> + <xsl:text> id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>").onclick = this.make_on_click("</xsl:text> + <xsl:value-of select="func:escape_quotes(@inkscape:label)"/> + <xsl:template match="widget[@type='Jump']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Jump widget brings focus to a different page. Mandatory first argument + <xsl:text>gives name of the page. + <xsl:text>If first path is pointing to HMI_NODE variable is used as new reference + <xsl:text>when jumping to a relative page. + <xsl:text>Additional arguments are unordered options: + <xsl:text>- Absolute: force page jump to be not relative even if first path is of type HMI_NODE + <xsl:text>- name=value: Notify PLC about jump by setting variable with path having same name assigned + <xsl:text>"active"+"inactive" labeled elements can be provided and reflect current + <xsl:text>page being shown. + <xsl:text>Relative jump: + <xsl:text>HMI:Jump:RelativePage@/PUMP9 + <xsl:text>HMI:Jump:RelativePage@/PUMP9@role=.userrole#role=="admin" + <xsl:text>Absolute jump: + <xsl:text>HMI:Jump:AbsolutePage + <xsl:text>HMI:Jump:AbsolutePage@role=.userrole#role=="admin" + <xsl:text>Forced absolute jump: + <xsl:text>HMI:Jump:AbsolutePage:Absolute@/PUMP9 + <xsl:text>HMI:Jump:AbsolutePage:Absolute:notify=1@notify=/PUMP9 + <xsl:text>Jump with feedback + <xsl:text>HMI:Jump:AbsolutePage:notify=1@notify=.did_jump + <xsl:text>Jump to given page</xsl:text> + <arg name="page" accepts="string"> + <xsl:text>name of page to jump to</xsl:text> + <path name="reference" count="optional" accepts="HMI_NODE"> + <xsl:text>reference for relative jump</xsl:text> + <xsl:template match="widget[@type='Jump']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>JumpWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> activable = false; + <xsl:text> frequency = 2; + <xsl:text> target_page_is_current_page = false; + <xsl:text> button_being_pressed = false; + <xsl:text> onmouseup(evt) { + <xsl:text> svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); + <xsl:text> if(this.enable_state) { + <xsl:text> const index = + <xsl:text> (this.is_relative && this.indexes.length > 0) ? + <xsl:text> this.indexes[0] + this.offset : undefined; + <xsl:text> this.button_being_pressed = false; + <xsl:text> this.activity_state = this.target_page_is_current_page || this.button_being_pressed; + <xsl:text> fading_page_switch(this.args[0], index); + <xsl:text> this.notify(); + <xsl:text> onmousedown(){ + <xsl:text> if(this.enable_state) { + <xsl:text> svg_root.addEventListener("pointerup", this.bound_onmouseup, true); + <xsl:text> this.button_being_pressed = true; + <xsl:text> this.activity_state = true; + <xsl:text> this.request_animate(); + <xsl:text> notify_page_change(page_name, index) { + <xsl:text> // called from animate() + <xsl:text> if(this.activable) { + <xsl:text> const ref_index = this.indexes.length > 0 ? this.indexes[0] + this.offset : undefined; + <xsl:text> const ref_name = this.args[0]; + <xsl:text> this.target_page_is_current_page = ((ref_name == undefined || ref_name == page_name) && index == ref_index); + <xsl:text> this.activity_state = this.target_page_is_current_page || this.button_being_pressed; + <xsl:text> // Since called from animate, update activity directly + <xsl:text> if(this.enable_displayed_state && this.has_activity) { + <xsl:text> this.animate_activity(); + <func:function name="func:is_relative_jump"> + <xsl:param name="widget"/> + <func:result select="$widget/path and $widget/path[1]/@type='HMI_NODE' and not($widget/arg[position()>1 and @value = 'Absolute'])"/> + <xsl:template match="widget[@type='Jump']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:text> activable_sub:{ + <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 name="mandatory"> + <xsl:text>no</xsl:text> + <xsl:value-of select="$activity"/> + <xsl:variable name="has_activity" select="string-length($activity)>0"/> + <xsl:text> has_activity: </xsl:text> + <xsl:value-of select="$has_activity"/> + <xsl:variable name="jump_disability" select="$has_activity and $has_disability"/> + <xsl:text> init: function() { + <xsl:text> this.bound_onmouseup = this.onmouseup.bind(this); + <xsl:text> this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); + <xsl:if test="$has_activity"> + <xsl:text> this.activable = true; + <xsl:text> this.is_relative = </xsl:text> + <xsl:when test="func:is_relative_jump(.)"> + <xsl:text>true</xsl:text> + <xsl:text>false</xsl:text> + <xsl:text> notify: function() { + <xsl:variable name="paths" select="path"/> + <xsl:for-each select="arg[position()>1 and 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:text> // </xsl:text> + <xsl:value-of select="@value"/> + <xsl:text> this.apply_hmi_value(</xsl:text> + <xsl:value-of select="$index"/> + <xsl:text>, </xsl:text> + <xsl:value-of select="$value"/> + <xsl:template match="widget[@type='Jump']" mode="widget_page"> + <xsl:param name="page_desc"/> + <xsl:param name="page_desc"/> + <xsl:if test="func:is_relative_jump(.)"> + <xsl:variable name="target_page_name"> + <xsl:value-of select="arg[1]/@value"/> + <xsl:value-of select="$page_desc/arg[1]/@value"/> + <xsl:variable name="target_page_path"> + <xsl:value-of select="$hmi_pages_descs[arg[1]/@value = $target_page_name]/path[not(@assign)]/@value"/> + <xsl:value-of select="$page_desc/path[not(@assign)]/@value"/> + <xsl:if test="not(func:same_class_paths($target_page_path, path[1]/@value))"> + <xsl:message terminate="yes"> + <xsl:text>Jump id="</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>" to page "</xsl:text> + <xsl:value-of select="$target_page_name"/> + <xsl:text>" with incompatible path "</xsl:text> + <xsl:value-of select="path[1]/@value"/> + <xsl:text> (must be same class as "</xsl:text> + <xsl:value-of select="$target_page_path"/> + <xsl:text>")</xsl:text> + <xsl:template match="cssdefs:jump"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:text>.fade-out-page { + <xsl:text> animation: cubic-bezier(0, 0.8, 0.6, 1) fadeOut 0.6s both; + <xsl:text>@keyframes fadeOut { + <xsl:text> 0% { opacity: 1; } + <xsl:text> 100% { opacity: 0; } + <xsl:template match="declarations:jump"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:text>var jumps_need_update = false; + <xsl:text>var jump_history = [[default_page, undefined]]; + <xsl:text>function update_jumps() { + <xsl:text> // called from animate() + <xsl:text> page_desc[current_visible_page].jumps.map(w=>w.notify_page_change(current_visible_page,current_page_index)); + <xsl:text> jumps_need_update = false; + <xsl:template match="widget[@type='Keypad']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Keypad - to be written + <xsl:text>Keypad </xsl:text> + <arg name="supported_types" accepts="string"> + <xsl:text>keypad can input those types </xsl:text> + <xsl:template match="declarations:keypad"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:text>var keypads = { + <xsl:for-each select="$keypads_descs"> + <xsl:variable name="keypad_id" select="@id"/> + <xsl:for-each select="arg"> + <xsl:variable name="g" select="$geometry[@Id = $keypad_id]"/> + <xsl:text> "</xsl:text> + <xsl:value-of select="@value"/> + <xsl:text>":["</xsl:text> + <xsl:value-of select="$keypad_id"/> + <xsl:text>", </xsl:text> + <xsl:value-of select="$g/@x"/> + <xsl:text>, </xsl:text> + <xsl:value-of select="$g/@y"/> + <xsl:template match="widget[@type='Keypad']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>KeypadWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> on_key_click(symbols) { + <xsl:text> var syms = symbols.split(" "); + <xsl:text> this.shift |= this.caps; + <xsl:text> if(this.virgin) + <xsl:text> this.editstr = ""; + <xsl:text> this.editstr += syms[this.shift?syms.length-1:0]; + <xsl:text> this.shift = false; + <xsl:text> this.update(); + <xsl:text> on_Esc_click() { + <xsl:text> end_modal.call(this); + <xsl:text> on_Enter_click() { + <xsl:text> let coercedval = (typeof this.initial) == "number" ? Number(this.editstr) : this.editstr; + <xsl:text> if(typeof coercedval == 'number' && isNaN(coercedval)){ + <xsl:text> // revert to initial so it explicitely shows input was ignored + <xsl:text> this.editstr = String(this.initial); + <xsl:text> this.update(); + <xsl:text> let callback_obj = this.result_callback_obj; + <xsl:text> end_modal.call(this); + <xsl:text> callback_obj.edit_callback(coercedval); + <xsl:text> on_BackSpace_click() { + <xsl:text> this.editstr = this.editstr.slice(0,this.editstr.length-1); + <xsl:text> this.update(); + <xsl:text> on_Sign_click() { + <xsl:text> if(this.editstr[0] == "-") + <xsl:text> this.editstr = this.editstr.slice(1,this.editstr.length); + <xsl:text> this.editstr = "-" + this.editstr; + <xsl:text> this.update(); + <xsl:text> on_NumDot_click() { + <xsl:text> if(this.editstr.indexOf(".") == "-1"){ + <xsl:text> this.editstr += "."; + <xsl:text> this.update(); + <xsl:text> on_Space_click() { + <xsl:text> this.editstr += " "; + <xsl:text> this.update(); + <xsl:text> caps = false; + <xsl:text> _caps = undefined; + <xsl:text> on_CapsLock_click() { + <xsl:text> this.caps = !this.caps; + <xsl:text> this.update(); + <xsl:text> shift = false; + <xsl:text> _shift = undefined; + <xsl:text> on_Shift_click() { + <xsl:text> this.shift = !this.shift; + <xsl:text> this.caps = false; + <xsl:text> this.update(); + <xsl:text> editstr = ""; + <xsl:text> _editstr = undefined; + <xsl:text> result_callback_obj = undefined; + <xsl:text> start_edit(info, valuetype, callback_obj, initial,size) { + <xsl:text> show_modal.call(this,size); + <xsl:text> this.editstr = String(initial); + <xsl:text> this.result_callback_obj = callback_obj; + <xsl:text> if(this.Info_elt) + <xsl:text> this.Info_elt.textContent = info; + <xsl:text> this.shift = false; + <xsl:text> this.caps = false; + <xsl:text> this.initial = initial; + <xsl:text> this.update(); + <xsl:text> this.virgin = true; + <xsl:text> if(this.editstr != this._editstr){ + <xsl:text> this.virgin = false; + <xsl:text> this._editstr = this.editstr; + <xsl:text> this.Value_elt.textContent = this.editstr; + <xsl:text> if(this.Shift_sub && this.shift != this._shift){ + <xsl:text> this._shift = this.shift; + <xsl:text> set_activity_state(this.Shift_sub, this.shift); + <xsl:text> if(this.CapsLock_sub && this.caps != this._caps){ + <xsl:text> this._caps = this.caps; + <xsl:text> set_activity_state(this.CapsLock_sub, this.caps); + <xsl:template match="widget[@type='Keypad']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>Esc Enter BackSpace Keys Value</xsl:text> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>Sign Space NumDot Info</xsl:text> + <xsl:with-param name="mandatory" select="'no'"/> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>CapsLock Shift</xsl:text> + <xsl:with-param name="mandatory" select="'no'"/> + <xsl:with-param name="subelements" select="'active inactive'"/> + <xsl:text> init: function() { + <xsl:for-each select="$hmi_element/*[@inkscape:label = 'Keys']/*"> + <xsl:text> id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>").setAttribute("onclick", "hmi_widgets['</xsl:text> + <xsl:value-of select="$hmi_element/@id"/> + <xsl:text>'].on_key_click('</xsl:text> + <xsl:value-of select="func:escape_quotes(@inkscape:label)"/> + <xsl:for-each select="str:split('Esc Enter BackSpace Sign Space NumDot CapsLock Shift')"> + <xsl:text> if(this.</xsl:text> + <xsl:value-of select="."/> + <xsl:text> this.</xsl:text> + <xsl:value-of select="."/> + <xsl:text>_elt.setAttribute("onclick", "hmi_widgets['</xsl:text> + <xsl:value-of select="$hmi_element/@id"/> + <xsl:text>'].on_</xsl:text> + <xsl:value-of select="."/> + <xsl:variable name="g" select="$geometry[@Id = $hmi_element/@id]"/> + <xsl:text> coordinates: [</xsl:text> + <xsl:value-of select="$g/@x"/> + <xsl:text>, </xsl:text> + <xsl:value-of select="$g/@y"/> + <xsl:text> virgin: false, + <xsl:template match="widget[@type='List']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>List widget is a svg:group, list items are labeled elements + <xsl:text>in that group. + <xsl:text>To use a List, clone (svg:use) one of the items inside the widget that + <xsl:text>expects a List. + <xsl:text>Positions of items are relative to each other, and they must all be in the + <xsl:text>same place. In order to make editing easier it is therefore recommanded to + <xsl:text>make stacked clones of svg elements spread nearby the list. + <xsl:text>A named list of named graphical elements</xsl:text> + <xsl:template match="widget[@type='List']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:for-each select="$hmi_element/*[@inkscape:label]"> + <xsl:text> "</xsl:text> + <xsl:value-of select="@inkscape:label"/> + <xsl:text>": "</xsl:text> + <xsl:value-of select="@id"/> + <xsl:template match="widget[@type='List']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>ListWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:template match="widget[@type='ListSwitch']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>ListSwitch widget displays one item of an HMI:List depending on value of + <xsl:text>given variable. Main element of the widget must be a clone of the list or + <xsl:text>of an item of that list. + <xsl:text>Given variable's current value is compared to list items + <xsl:text>label. For exemple if given variable type + <xsl:text>is HMI_INT and value is 1, then item with label '1' will be displayed. + <xsl:text>If matching variable of type HMI_STRING, then no quotes are needed. + <xsl:text>For exemple, 'hello' match HMI_STRING 'hello'. + <xsl:text>Displays item of an HMI:List whose label matches value.</xsl:text> + <path name="value" accepts="HMI_INT,HMI_STRING"> + <xsl:text>value to compare to labels</xsl:text> + <xsl:template match="widget[@type='ListSwitch']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>ListSwitchWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 5; + <xsl:template match="widget[@type='ListSwitch']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:variable name="targetid" select="substring-after($hmi_element/@xlink:href,'#')"/> + <xsl:variable name="from_list" select="$hmi_lists[(@id | */@id) = $targetid]"/> + <xsl:text> dispatch: function(value) { + <xsl:text> this.element.href.baseVal = "#"+hmi_widgets["</xsl:text> + <xsl:value-of select="$from_list/@id"/> + <xsl:text>"].items[value]; + <xsl:template match="widget[@type='Meter']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Meter widget moves the end of "needle" labeled path along "range" labeled + <xsl:text>path, according to value of the single accepted variable. + <xsl:text>Needle is reduced to a single segment. If "min" a "max" labeled texts + <xsl:text>are provided, or if first and second argument are given, then they are used + <xsl:text>as respective minimum and maximum value. Otherwise, value is expected to be + <xsl:text>in between 0 and 100. + <xsl:text>Moves "needle" along "range"</xsl:text> + <arg name="min" count="optional" accepts="int,real"> + <xsl:text>minimum value</xsl:text> + <arg name="max" count="optional" accepts="int,real"> + <xsl:text>maximum value</xsl:text> + <path name="value" accepts="HMI_INT,HMI_REAL"> + <xsl:text>Value to display</xsl:text> + <xsl:template match="widget[@type='Meter']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>MeterWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 10; + <xsl:text> origin = undefined; + <xsl:text> range = undefined; + <xsl:text> dispatch(value) { + <xsl:text> this.display_val = value; + <xsl:text> this.request_animate(); + <xsl:text> if(this.value_elt) + <xsl:text> this.value_elt.textContent = String(this.display_val); + <xsl:text> let [min,max,totallength] = this.range; + <xsl:text> let length = Math.max(0,Math.min(totallength,(Number(this.display_val)-min)*totallength/(max-min))); + <xsl:text> let tip = this.range_elt.getPointAtLength(length); + <xsl:text> this.needle_elt.setAttribute('d', "M "+this.origin.x+","+this.origin.y+" "+tip.x+","+tip.y); + <xsl:text> let [min,max] = [[this.min_elt,0],[this.max_elt,100]].map(([elt,def],i)=>elt? + <xsl:text> Number(elt.textContent) : + <xsl:text> this.args.length >= i+1 ? this.args[i] : def); + <xsl:text> this.range = [min, max, this.range_elt.getTotalLength()] + <xsl:text> this.origin = this.needle_elt.getPointAtLength(0); + <xsl:template match="widget[@type='Meter']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>needle range</xsl:text> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>min max</xsl:text> + <xsl:with-param name="mandatory" select="'no'"/> + <xsl:template match="widget[@type='MultiState']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:text>Mutlistateh widget hides all subelements whose label do not match given + <xsl:text>variable value representation. For exemple if given variable type + <xsl:text>is HMI_INT and value is 1, then elements with label '1' will be displayed. + <xsl:text>Label can have comments, so '1#some comment' would also match. If matching + <xsl:text>variable of type HMI_STRING, then double quotes must be used. For exemple, + <xsl:text>'"hello"' or '"hello"#another comment' match HMI_STRING 'hello'. + <xsl:text>Click on widget changes variable value to next value in given list, or to + <xsl:text>first one if not initialized to value already part of the list. + <xsl:text>Show elements whose label match value.</xsl:text> + <path name="value" accepts="HMI_INT,HMI_STRING"> + <xsl:text>value to compare to labels</xsl:text> + <xsl:template match="widget[@type='MultiState']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>MultiStateWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 5; + <xsl:text> dispatch(value) { + <xsl:text> this.state = value; + <xsl:text> for(let choice of this.choices){ + <xsl:text> if(this.state != choice.value){ + <xsl:text> choice.elt.setAttribute("style", "display:none"); + <xsl:text> choice.elt.setAttribute("style", choice.style); + <xsl:text> // TODO : use RequestAnimate and animate() + <xsl:text> on_click(evt) { + <xsl:text> //get current selected value + <xsl:text> let next_ind; + <xsl:text> for(next_ind=0; next_ind<this.choices.length; next_ind++){ + <xsl:text> if(this.state == this.choices[next_ind].value){ + <xsl:text> next_ind = next_ind + 1; + <xsl:text> //get next selected value + <xsl:text> if(this.choices.length > next_ind){ + <xsl:text> this.state = this.choices[next_ind].value; + <xsl:text> this.state = this.choices[0].value; + <xsl:text> //post value to plc + <xsl:text> this.apply_hmi_value(0, this.state); + <xsl:text> this.element.setAttribute("onclick", "hmi_widgets['"+this.element_id+"'].on_click(evt)"); + <xsl:template match="widget[@type='MultiState']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:variable name="regex" select="'^("[^"].*"|\-?[0-9]+|false|true)(#.*)?$'"/> + <xsl:for-each select="$result_svg_ns//*[@id = $hmi_element/@id]//*[regexp:test(@inkscape:label,$regex)]"> + <xsl:variable name="literal" select="regexp:match(@inkscape:label,$regex)[2]"/> + <xsl:text> elt:id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text> style:"</xsl:text> + <xsl:value-of select="@style"/> + <xsl:text> value:</xsl:text> + <xsl:value-of select="$literal"/> + <xsl:text> }</xsl:text> + <xsl:if test="position()!=last()"> + <xsl:template match="widget[@type='Page']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Arguments are either: + <xsl:text>- XXX reference path TODO + <xsl:text>- name=value: setting variable with literal value. + <xsl:text>- name=other_name: copy variable content into another + <xsl:text>HMI:Page:notify=1@notify=/PLCVAR + <xsl:text>HMI:Page:ack=2:notify=1@ack=.local_var@notify=/PLCVAR + <xsl:text>Page </xsl:text> + <xsl:template match="widget[@type='Page']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:text> assignments: {}, + <xsl:text> dispatch: function(value, oldval, varnum) { + <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> assign: function() { + <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:variable name="isVarName" select="regexp:test($value,'^[a-zA-Z_][a-zA-Z0-9_]+$')"/> + <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> if(</xsl:text> + <xsl:value-of select="$value"/> + <xsl:text> != undefined) + <xsl:text> this.apply_hmi_value(</xsl:text> + <xsl:value-of select="$index"/> + <xsl:text>, </xsl:text> + <xsl:value-of select="$value"/> + <xsl:text> this.apply_hmi_value(</xsl:text> + <xsl:value-of select="$index"/> + <xsl:text>, </xsl:text> + <xsl:value-of select="$value"/> + <xsl:template match="widget[@type='PathSlider']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Slide an SVG element along a path by dragging it</xsl:text> + <path name="value" accepts="HMI_INT,HMI_REAL"> + <xsl:text>value</xsl:text> + <path name="min" count="optional" accepts="HMI_INT,HMI_REAL"> + <xsl:text>min</xsl:text> + <path name="max" count="optional" accepts="HMI_INT,HMI_REAL"> + <xsl:text>max</xsl:text> + <arg name="min" count="optional" accepts="int,real"> + <xsl:text>minimum value</xsl:text> + <arg name="max" count="optional" accepts="int,real"> + <xsl:text>maximum value</xsl:text> + <xsl:template match="widget[@type='PathSlider']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>PathSliderWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 10; + <xsl:text> position = undefined; + <xsl:text> scannedPoints = []; + <xsl:text> pathLength = undefined; + <xsl:text> precision = undefined; + <xsl:text> origPt = undefined; + <xsl:text> scanPath() { + <xsl:text> this.pathLength = this.path_elt.getTotalLength(); + <xsl:text> this.precision = Math.floor(this.pathLength / 10); + <xsl:text> // save linear scan for coarse approximation + <xsl:text> for (var scanLength = 0; scanLength <= this.pathLength; scanLength += this.precision) { + <xsl:text> this.scannedPoints.push([this.path_elt.getPointAtLength(scanLength), scanLength]); + <xsl:text> [this.origPt,] = this.scannedPoints[0]; + <xsl:text> closestPoint(point) { + <xsl:text> var bestPoint, + <xsl:text> bestDistance = Infinity, + <xsl:text> scanDistance; + <xsl:text> // use linear scan for coarse approximation + <xsl:text> for (let [scanPoint, scanLength] of this.scannedPoints){ + <xsl:text> if ((scanDistance = distance2(scanPoint)) < bestDistance) { + <xsl:text> bestPoint = scanPoint, + <xsl:text> bestLength = scanLength, + <xsl:text> bestDistance = scanDistance; + <xsl:text> // binary search for more precise estimate + <xsl:text> let precision = this.precision / 2; + <xsl:text> while (precision > 0.5) { + <xsl:text> var beforePoint, + <xsl:text> beforeLength, + <xsl:text> afterLength, + <xsl:text> beforeDistance, + <xsl:text> afterDistance; + <xsl:text> if ((beforeLength = bestLength - precision) >= 0 && + <xsl:text> (beforeDistance = distance2(beforePoint = this.path_elt.getPointAtLength(beforeLength))) < bestDistance) { + <xsl:text> bestPoint = beforePoint, + <xsl:text> bestLength = beforeLength, + <xsl:text> bestDistance = beforeDistance; + <xsl:text> } else if ((afterLength = bestLength + precision) <= this.pathLength && + <xsl:text> (afterDistance = distance2(afterPoint = this.path_elt.getPointAtLength(afterLength))) < bestDistance) { + <xsl:text> bestPoint = afterPoint, + <xsl:text> bestLength = afterLength, + <xsl:text> bestDistance = afterDistance; + <xsl:text> precision /= 2; + <xsl:text> return [bestPoint, bestLength]; + <xsl:text> function distance2(p) { + <xsl:text> var dx = p.x - point.x, + <xsl:text> dy = p.y - point.y; + <xsl:text> return dx * dx + dy * dy; + <xsl:text> dispatch(value,oldval, index) { + <xsl:text> switch(index) { + <xsl:text> this.position = value; + <xsl:text> this.min = value; + <xsl:text> this.max = value; + <xsl:text> this.request_animate(); + <xsl:text> get_current_point(){ + <xsl:text> let currLength = this.pathLength * (this.position - this.min) / (this.max - this.min) + <xsl:text> return this.path_elt.getPointAtLength(currLength); + <xsl:text> if(this.position == undefined) + <xsl:text> let currPt = this.get_current_point(); + <xsl:text> this.cursor_transform.setTranslate(currPt.x - this.origPt.x, currPt.y - this.origPt.y); + <xsl:text> if(this.args.length == 2) + <xsl:text> [this.min, this.max]=this.args; + <xsl:text> this.scanPath(); + <xsl:text> this.cursor_transform = svg_root.createSVGTransform(); + <xsl:text> this.cursor_elt.transform.baseVal.appendItem(this.cursor_transform); + <xsl:text> this.cursor_elt.onpointerdown = (e) => this.on_cursor_down(e); + <xsl:text> this.bound_drag = this.drag.bind(this); + <xsl:text> this.bound_drop = this.drop.bind(this); + <xsl:text> start_dragging_from_event(e){ + <xsl:text> let clientPoint = new DOMPoint(e.clientX, e.clientY); + <xsl:text> let point = clientPoint.matrixTransform(this.invctm); + <xsl:text> let currPt = this.get_current_point(); + <xsl:text> this.draggingOffset = new DOMPoint(point.x - currPt.x , point.y - currPt.y); + <xsl:text> apply_position_from_event(e){ + <xsl:text> let clientPoint = new DOMPoint(e.clientX, e.clientY); + <xsl:text> let rawPoint = clientPoint.matrixTransform(this.invctm); + <xsl:text> let point = new DOMPoint(rawPoint.x - this.draggingOffset.x , rawPoint.y - this.draggingOffset.y); + <xsl:text> let [closestPoint, closestLength] = this.closestPoint(point); + <xsl:text> let new_position = this.min + (this.max - this.min) * closestLength / this.pathLength; + <xsl:text> this.position = Math.round(Math.max(Math.min(new_position, this.max), this.min)); + <xsl:text> this.apply_hmi_value(0, this.position); + <xsl:text> on_cursor_down(e){ + <xsl:text> // get scrollbar -> root transform + <xsl:text> let ctm = this.path_elt.getCTM(); + <xsl:text> // root -> path transform + <xsl:text> this.invctm = ctm.inverse(); + <xsl:text> this.start_dragging_from_event(e); + <xsl:text> svg_root.addEventListener("pointerup", this.bound_drop, true); + <xsl:text> svg_root.addEventListener("pointermove", this.bound_drag, true); + <xsl:text> svg_root.removeEventListener("pointerup", this.bound_drop, true); + <xsl:text> svg_root.removeEventListener("pointermove", this.bound_drag, true); + <xsl:text> this.apply_position_from_event(e); + <xsl:template match="widget[@type='PathSlider']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>cursor path</xsl:text> + <xsl:template match="widget[@type='ScrollBar']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>ScrollBar - svg:rect based scrollbar + <xsl:text>ScrollBar</xsl:text> + <path name="value" accepts="HMI_INT"> + <xsl:text>value</xsl:text> + <path name="range" accepts="HMI_INT"> + <xsl:text>range</xsl:text> + <path name="visible" accepts="HMI_INT"> + <xsl:text>visible</xsl:text> + <xsl:template match="widget[@type='ScrollBar']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>ScrollBarWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 10; + <xsl:text> position = undefined; + <xsl:text> range = undefined; + <xsl:text> size = undefined; + <xsl:text> mincursize = 0.1; + <xsl:text> dispatch(value,oldval, index) { + <xsl:text> switch(index) { + <xsl:text> this.range = Math.max(1,value); + <xsl:text> this.position = value; + <xsl:text> this.size = value; + <xsl:text> this.request_animate(); + <xsl:text> get_ratios() { + <xsl:text> let range = this.range; + <xsl:text> let size = Math.max(range * this.mincursize, Math.min(this.size, range)); + <xsl:text> let maxh = this.range_elt.height.baseVal.value; + <xsl:text> let pixels = maxh; + <xsl:text> return [size, maxh, range, pixels]; + <xsl:text> if(this.position == undefined || this.range == undefined || this.size == undefined) + <xsl:text> let [size, maxh, range, pixels] = this.get_ratios(); + <xsl:text> let new_y = this.range_elt.y.baseVal.value + Math.round(Math.min(this.position,range-size) * pixels / range); + <xsl:text> let new_height = Math.round(maxh * size/range); + <xsl:text> this.cursor_elt.y.baseVal.value = new_y; + <xsl:text> this.cursor_elt.height.baseVal.value = new_height; + <xsl:text> init_mandatory() { + <xsl:text> this.cursor_elt.onpointerdown = () => this.on_cursor_down(); + <xsl:text> this.bound_drag = this.drag.bind(this); + <xsl:text> this.bound_drop = this.drop.bind(this); + <xsl:text> apply_position(position){ + <xsl:text> this.position = Math.round(Math.max(Math.min(position, this.range - this.size), 0)); + <xsl:text> this.apply_hmi_value(1, this.position); + <xsl:text> on_page_click(is_up){ + <xsl:text> this.apply_position(is_up ? this.position-this.size + <xsl:text> : this.position+this.size); + <xsl:text> on_cursor_down(e){ + <xsl:text> // get scrollbar -> root transform + <xsl:text> let ctm = this.range_elt.getCTM(); + <xsl:text> // relative motion -> discard translation + <xsl:text> // root -> scrollbar transform + <xsl:text> this.invctm = ctm.inverse(); + <xsl:text> svg_root.addEventListener("pointerup", this.bound_drop, true); + <xsl:text> svg_root.addEventListener("pointermove", this.bound_drag, true); + <xsl:text> this.dragpos = this.position; + <xsl:text> svg_root.removeEventListener("pointerup", this.bound_drop, true); + <xsl:text> svg_root.removeEventListener("pointermove", this.bound_drag, true); + <xsl:text> let [size, maxh, range, pixels] = this.get_ratios(); + <xsl:text> if(pixels == 0) return; + <xsl:text> let point = new DOMPoint(e.movementX, e.movementY); + <xsl:text> let movement = point.matrixTransform(this.invctm).y; + <xsl:text> this.dragpos += movement * range / pixels; + <xsl:text> this.apply_position(this.dragpos); + <xsl:template match="widget[@type='ScrollBar']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>cursor range</xsl:text> + <xsl:variable name="pagebuttons"> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>pageup pagedown</xsl:text> + <xsl:with-param name="mandatory" select="'no'"/> + <xsl:variable name="have_pagebuttons" select="string-length($pagebuttons)>0"/> + <xsl:value-of select="$pagebuttons"/> + <xsl:text> init: function() { + <xsl:text> this.init_mandatory(); + <xsl:if test="$have_pagebuttons"> + <xsl:text> this.pageup_elt.onclick = () => this.on_page_click(true); + <xsl:text> this.pagedown_elt.onclick = () => this.on_page_click(false); + <xsl:template match="widget[@type='Switch']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Switch widget hides all subelements whose label do not match given + <xsl:text>variable current value representation. For exemple if given variable type + <xsl:text>is HMI_INT and value is 1, then elements with label '1' will be displayed. + <xsl:text>Label can have comments, so '1#some comment' would also match. If matching + <xsl:text>variable of type HMI_STRING, then double quotes must be used. For exemple, + <xsl:text>'"hello"' or '"hello"#another comment' match HMI_STRING 'hello'. + <xsl:text>Show elements whose label matches value.</xsl:text> + <path name="value" accepts="HMI_INT,HMI_STRING"> + <xsl:text>value to compare to labels</xsl:text> + <xsl:template match="widget[@type='Switch']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>SwitchWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 5; + <xsl:text> current_value = undefined; + <xsl:text> this.animate(); + <xsl:text> dispatch(value) { + <xsl:text> this.current_value = value; + <xsl:text> this.request_animate(); + <xsl:text> for(let choice of this.choices){ + <xsl:text> if(this.current_value != choice.value){ + <xsl:text> if(choice.parent == undefined){ + <xsl:text> choice.parent = choice.elt.parentElement; + <xsl:text> choice.parent.removeChild(choice.elt); + <xsl:text> if(choice.parent != undefined){ + <xsl:text> choice.parent.insertBefore(choice.elt,choice.sibling); + <xsl:text> choice.parent = undefined; + <xsl:template match="widget[@type='Switch']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:variable name="regex" select="'^("[^"].*"|\-?[0-9]+|false|true)(#.*)?$'"/> + <xsl:variable name="subelts" select="$result_widgets[@id = $hmi_element/@id]//*"/> + <xsl:variable name="subwidgets" select="$subelts//*[@id = $hmi_widgets/@id]"/> + <xsl:variable name="accepted" select="$subelts[not(ancestor-or-self::*/@id = $subwidgets/@id)]"/> + <xsl:variable name="choices" select="$accepted[regexp:test(@inkscape:label,$regex)]"/> + <xsl:for-each select="$choices"> + <xsl:variable name="literal" select="regexp:match(@inkscape:label,$regex)[2]"/> + <xsl:variable name="sibling" select="following-sibling::*[not(@id = $choices/@id)][position()=1]"/> + <xsl:text> elt:id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text> parent:undefined, + <xsl:when test="count($sibling)=0"> + <xsl:text> sibling:null, + <xsl:text> sibling:id("</xsl:text> + <xsl:value-of select="$sibling/@id"/> + <xsl:text> value:</xsl:text> + <xsl:value-of select="$literal"/> + <xsl:text> }</xsl:text> + <xsl:if test="position()!=last()"> + <xsl:template match="widget[@type='TextList']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>TextList widget is a svg:group, list items are labeled elements + <xsl:text>in that group. + <xsl:text>To use a TextList, clone (svg:use) one of the items inside the widget + <xsl:text>that expects a TextList. + <xsl:text>In this list, (translated) text content is what matters. Nevertheless + <xsl:text>text style of the cloned item will be applied in client widget. + <xsl:text>A named list of ordered texts </xsl:text> + <xsl:template match="widget[@type='TextList']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:for-each select="func:refered_elements($hmi_element/*[@inkscape:label])[self::svg:text]"> + <xsl:text> id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text> ].reverse(), + <xsl:template match="widget[@type='TextList']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>TextListWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:template match="widget[@type='TextStyleList']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>TextStyleList widget is a svg:group, list items are labeled elements + <xsl:text>in that group. + <xsl:text>To use a TextStyleList, clone (svg:use) one of the items inside the widget + <xsl:text>that expects a TextStyleList. + <xsl:text>In this list, only style matters. Text content is ignored. + <xsl:text>A named list of named texts</xsl:text> + <xsl:template match="widget[@type='TextStyleList']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:for-each select="$hmi_element/*[@inkscape:label]"> + <xsl:variable name="style" select="func:refered_elements(.)[self::svg:text]/@style"/> + <xsl:value-of select="@inkscape:label"/> + <xsl:text>: "</xsl:text> + <xsl:value-of select="$style"/> + <xsl:template match="widget[@type='TextStyleList']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>TextStyleListWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:template match="widget[@type='ToggleButton']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Button widget takes one boolean variable path, and reflect current true + <xsl:text>or false value by showing "active" or "inactive" labeled element + <xsl:text>respectively. Clicking or touching button toggles variable. + <xsl:text>Toggle button reflecting given boolean variable</xsl:text> + <path name="value" accepts="HMI_BOOL"> + <xsl:text>Boolean variable</xsl:text> + <xsl:template match="widget[@type='ToggleButton']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>ToggleButtonWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 5; + <xsl:text> active_style = undefined; + <xsl:text> inactive_style = undefined; + <xsl:text> dispatch(value) { + <xsl:text> this.activity_state = Boolean(value); + <xsl:text> //redraw toggle button + <xsl:text> this.request_animate(); + <xsl:text> on_click(evt) { + <xsl:text> //toggle state and apply + <xsl:text> this.activity_state = this.activity_state ? false : true; + <xsl:text> this.apply_hmi_value(0, this.activity_state); + <xsl:text> //redraw toggle button + <xsl:text> this.request_animate(); + <xsl:text> this.element.onclick = (evt) => this.on_click(evt); + <xsl:text> this.activity_state = undefined; + <xsl:template match="widget[@type='ToggleButton']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:text> activable_sub:{ + <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 name="mandatory"> + <xsl:text>warn</xsl:text> + <xsl:value-of select="$activity"/> + <xsl:variable name="has_activity" select="string-length($activity)>0"/> + <xsl:text> has_activity: </xsl:text> + <xsl:value-of select="$has_activity"/> + <xsl:template match="widget[@type='XYGraph']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>XYGraph draws a cartesian trend graph re-using styles given for axis, + <xsl:text>grid/marks, legends and curves. + <xsl:text>Elements labeled "x_axis" and "y_axis" are svg:groups containg: + <xsl:text> - "axis_label" svg:text gives style an alignment for axis labels. + <xsl:text> - "interval_major_mark" and "interval_minor_mark" are svg elements to be + <xsl:text> duplicated along axis line to form intervals marks. + <xsl:text> - "axis_line" svg:path is the axis line. Paths must be intersect and their + <xsl:text> bounding box is the chart wall. + <xsl:text>Elements labeled "curve_0", "curve_1", ... are paths whose styles are used + <xsl:text>to draw curves corresponding to data from variables passed as HMI tree paths. + <xsl:text>"curve_0" is mandatory. HMI variables outnumbering given curves are ignored. + <xsl:text>Cartesian trend graph showing values of given variables over time</xsl:text> + <path name="value" count="1+" accepts="HMI_INT,HMI_REAL"> + <xsl:text>value</xsl:text> + <arg name="xrange" accepts="int,time"> + <xsl:text>X axis range expressed either in samples or duration.</xsl:text> + <arg name="xformat" count="optional" accepts="string"> + <xsl:text>format string for X label</xsl:text> + <arg name="yformat" count="optional" accepts="string"> + <xsl:text>format string for Y label</xsl:text> + <xsl:template match="widget[@type='XYGraph']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>XYGraphWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 1; + <xsl:text> let x_duration_s; + <xsl:text> [x_duration_s, + <xsl:text> this.x_format, this.y_format] = this.args; + <xsl:text> let timeunit = x_duration_s.slice(-1); + <xsl:text> let factor = { + <xsl:text> "d":86400}[timeunit]; + <xsl:text> if(factor == undefined){ + <xsl:text> this.max_data_length = Number(x_duration_s); + <xsl:text> this.x_duration = undefined; + <xsl:text> let duration = factor*Number(x_duration_s.slice(0,-1)); + <xsl:text> this.max_data_length = undefined; + <xsl:text> this.x_duration = duration*1000; + <xsl:text> // Min and Max given with paths are meant to describe visible range, + <xsl:text> // not to clip data. + <xsl:text> this.clip = false; + <xsl:text> let y_min = Infinity, y_max = -Infinity; + <xsl:text> // Compute visible Y range by merging fixed curves Y ranges + <xsl:text> for(let varopts of this.variables_options){ + <xsl:text> let minmax = varopts.minmax + <xsl:text> let [min,max] = minmax; + <xsl:text> if(min < y_min) + <xsl:text> y_min = min; + <xsl:text> if(max > y_max) + <xsl:text> y_max = max; + <xsl:text> if(y_min !== Infinity && y_max !== -Infinity){ + <xsl:text> this.fixed_y_range = true; + <xsl:text> this.fixed_y_range = false; + <xsl:text> this.ymin = y_min; + <xsl:text> this.ymax = y_max; + <xsl:text> this.curves = []; + <xsl:text> this.init_specific(); + <xsl:text> this.reference = new ReferenceFrame( + <xsl:text> [[this.x_interval_minor_mark_elt, this.x_interval_major_mark_elt], + <xsl:text> [this.y_interval_minor_mark_elt, this.y_interval_major_mark_elt]], + <xsl:text> [this.x_axis_label_elt, this.y_axis_label_elt], + <xsl:text> [this.x_axis_line_elt, this.y_axis_line_elt], + <xsl:text> [this.x_format, this.y_format]); + <xsl:text> let max_stroke_width = 0; + <xsl:text> for(let curve of this.curves){ + <xsl:text> if(curve.style.strokeWidth > max_stroke_width){ + <xsl:text> max_stroke_width = curve.style.strokeWidth; + <xsl:text> this.Margins=this.reference.getLengths().map(length => max_stroke_width/length); + <xsl:text> // create <clipPath> path and attach it to widget + <xsl:text> let clipPath = document.createElementNS(xmlns,"clipPath"); + <xsl:text> let clipPathPath = document.createElementNS(xmlns,"path"); + <xsl:text> let clipPathPathDattr = document.createAttribute("d"); + <xsl:text> clipPathPathDattr.value = this.reference.getClipPathPathDattr(); + <xsl:text> clipPathPath.setAttributeNode(clipPathPathDattr); + <xsl:text> clipPath.appendChild(clipPathPath); + <xsl:text> clipPath.id = randomId(); + <xsl:text> this.element.appendChild(clipPath); + <xsl:text> // assign created clipPath to clip-path property of curves + <xsl:text> for(let curve of this.curves){ + <xsl:text> curve.setAttribute("clip-path", "url(#" + clipPath.id + ")"); + <xsl:text> this.curves_data = []; + <xsl:text> dispatch(value,oldval, index) { + <xsl:text> // TODO: get PLC time instead of browser time + <xsl:text> let time = Date.now(); + <xsl:text> // naive local buffer impl. + <xsl:text> // data is updated only when graph is visible + <xsl:text> // TODO: replace with separate recording + <xsl:text> if(this.curves_data[index] === undefined){ + <xsl:text> this.curves_data[index] = []; + <xsl:text> this.curves_data[index].push([time, value]); + <xsl:text> let data_length = this.curves_data[index].length; + <xsl:text> let ymin_damaged = false; + <xsl:text> let ymax_damaged = false; + <xsl:text> let overflow; + <xsl:text> if(this.max_data_length == undefined){ + <xsl:text> let peremption = time - this.x_duration; + <xsl:text> let oldest = this.curves_data[index][0][0] + <xsl:text> this.xmin = peremption; + <xsl:text> if(oldest < peremption){ + <xsl:text> // remove first item + <xsl:text> overflow = this.curves_data[index].shift()[1]; + <xsl:text> data_length = data_length - 1; + <xsl:text> if(data_length > this.max_data_length){ + <xsl:text> // remove first item + <xsl:text> [this.xmin, overflow] = this.curves_data[index].shift(); + <xsl:text> data_length = data_length - 1; + <xsl:text> if(this.xmin == undefined){ + <xsl:text> this.xmin = time; + <xsl:text> this.xmax = time; + <xsl:text> let Xrange = this.xmax - this.xmin; + <xsl:text> if(!this.fixed_y_range){ + <xsl:text> ymin_damaged = overflow <= this.ymin; + <xsl:text> ymax_damaged = overflow >= this.ymax; + <xsl:text> if(value > this.ymax){ + <xsl:text> ymax_damaged = false; + <xsl:text> this.ymax = value; + <xsl:text> if(value < this.ymin){ + <xsl:text> ymin_damaged = false; + <xsl:text> this.ymin = value; + <xsl:text> let Yrange = this.ymax - this.ymin; + <xsl:text> // apply margin by moving min and max to enlarge range + <xsl:text> let [xMargin,yMargin] = zip(this.Margins, [Xrange, Yrange]).map(([m,l]) => m*l); + <xsl:text> [[this.dxmin, this.dxmax],[this.dymin,this.dymax]] = + <xsl:text> [[this.xmin-xMargin, this.xmax+xMargin], + <xsl:text> [this.ymin-yMargin, this.ymax+yMargin]]; + <xsl:text> Xrange += 2*xMargin; + <xsl:text> Yrange += 2*yMargin; + <xsl:text> // recompute curves "d" attribute + <xsl:text> // FIXME: use SVG getPathData and setPathData when available. + <xsl:text> // https://svgwg.org/specs/paths/#InterfaceSVGPathData + <xsl:text> // https://github.com/jarek-foksa/path-data-polyfill + <xsl:text> let [base_point, xvect, yvect] = this.reference.getBaseRef(); + <xsl:text> this.curves_d_attr = + <xsl:text> zip(this.curves_data, this.curves).map(([data,curve]) => { + <xsl:text> let new_d = data.map(([x,y], i) => { + <xsl:text> // compute curve point from data, ranges, and base_ref + <xsl:text> let xv = vectorscale(xvect, (x - this.dxmin) / Xrange); + <xsl:text> let yv = vectorscale(yvect, (y - this.dymin) / Yrange); + <xsl:text> let px = base_point.x + xv.x + yv.x; + <xsl:text> let py = base_point.y + xv.y + yv.y; + <xsl:text> if(!this.fixed_y_range){ + <xsl:text> // update min and max from curve data if needed + <xsl:text> if(ymin_damaged && y < this.ymin) this.ymin = y; + <xsl:text> if(ymax_damaged && y > this.ymax) this.ymax = y; + <xsl:text> return " " + px + "," + py; + <xsl:text> new_d.unshift("M "); + <xsl:text> return new_d.join(''); + <xsl:text> // computed curves "d" attr is applied to svg curve during animate(); + <xsl:text> this.request_animate(); + <xsl:text> // move elements only if enough data + <xsl:text> if(this.curves_data.some(data => data.length > 1)){ + <xsl:text> // move marks and update labels + <xsl:text> this.reference.applyRanges([[this.dxmin, this.dxmax], + <xsl:text> [this.dymin, this.dymax]]); + <xsl:text> // apply computed curves "d" attributes + <xsl:text> for(let [curve, d_attr] of zip(this.curves, this.curves_d_attr)){ + <xsl:text> curve.setAttribute("d", d_attr); + <func:function name="func:check_curves_label_consistency"> + <xsl:param name="curve_elts"/> + <xsl:param name="number_to_check"/> + <xsl:variable name="res"> + <xsl:when test="$curve_elts[@inkscape:label = concat('curve_', string($number_to_check))]"> + <xsl:if test="$number_to_check > 0"> + <xsl:value-of select="func:check_curves_label_consistency($curve_elts, $number_to_check - 1)"/> + <xsl:value-of select="concat('missing curve_', string($number_to_check))"/> + <func:result select="$res"/> + <xsl:template match="widget[@type='XYGraph']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>/x_interval_minor_mark /x_axis_line /x_interval_major_mark /x_axis_label</xsl:text> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>/y_interval_minor_mark /y_axis_line /y_interval_major_mark /y_axis_label</xsl:text> + <xsl:text> init_specific() { + <xsl:variable name="curves" select="$hmi_element/*[regexp:test(@inkscape:label,'^curve_[0-9]+$')]"/> + <xsl:variable name="curves_error" select="func:check_curves_label_consistency($curves,count($curves)-1)"/> + <xsl:if test="string-length($curves_error)"> + <xsl:message terminate="yes"> + <xsl:text>XYGraph id="</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>", label="</xsl:text> + <xsl:value-of select="@inkscape:label"/> + <xsl:text>" : </xsl:text> + <xsl:value-of select="$curves_error"/> + <xsl:for-each select="$curves"> + <xsl:variable name="label" select="@inkscape:label"/> + <xsl:variable name="_id" select="@id"/> + <xsl:variable name="curve_num" select="substring(@inkscape:label, 7)"/> + <xsl:text> this.curves[</xsl:text> + <xsl:value-of select="$curve_num"/> + <xsl:text>] = id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>"); /* </xsl:text> + <xsl:value-of select="@inkscape:label"/> + <declarations:XYGraph/> + <xsl:template match="declarations:XYGraph"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:text>function lineFromPath(path_elt) { + <xsl:text> let start = path_elt.getPointAtLength(0); + <xsl:text> let end = path_elt.getPointAtLength(path_elt.getTotalLength()); + <xsl:text> return [start, new DOMPoint(end.x - start.x , end.y - start.y)]; + <xsl:text>function vector(p1, p2) { + <xsl:text> return new DOMPoint(p2.x - p1.x , p2.y - p1.y); + <xsl:text>function vectorscale(p1, p2) { + <xsl:text> return new DOMPoint(p2 * p1.x , p2 * p1.y); + <xsl:text>function vectorLength(p1) { + <xsl:text> return Math.sqrt(p1.x*p1.x + p1.y*p1.y); + <xsl:text>function randomId(){ + <xsl:text> return Date.now().toString(36) + Math.random().toString(36).substr(2); + <xsl:text>function move_elements_to_group(elements) { + <xsl:text> let newgroup = document.createElementNS(xmlns,"g"); + <xsl:text> newgroup.id = randomId(); + <xsl:text> for(let element of elements){ + <xsl:text> let parent = element.parentElement; + <xsl:text> if(parent !== null) + <xsl:text> parent.removeChild(element); + <xsl:text> newgroup.appendChild(element); + <xsl:text> return newgroup; + <xsl:text>function getLinesIntesection(l1, l2) { + <xsl:text> let [l1start, l1vect] = l1; + <xsl:text> let [l2start, l2vect] = l2; + <xsl:text> Compute intersection of two lines + <xsl:text> ================================= + <xsl:text> l1start ----------X--------------> l1vect + <xsl:text> / intersection + <xsl:text> let [x1, y1, x3, y3] = [l1start.x, l1start.y, l2start.x, l2start.y]; + <xsl:text> let [x2, y2, x4, y4] = [x1+l1vect.x, y1+l1vect.y, x3+l2vect.x, y3+l2vect.y]; + <xsl:text> // line intercept math by Paul Bourke http://paulbourke.net/geometry/pointlineplane/ + <xsl:text> // Determine the intersection point of two line segments + <xsl:text> // Return FALSE if the lines don't intersect + <xsl:text> // Check if none of the lines are of length 0 + <xsl:text> if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) { + <xsl:text> return false + <xsl:text> denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)) + <xsl:text> // Lines are parallel + <xsl:text> if (denominator === 0) { + <xsl:text> return false + <xsl:text> let ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator + <xsl:text> let ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator + <xsl:text> // Return a object with the x and y coordinates of the intersection + <xsl:text> let x = x1 + ua * (x2 - x1) + <xsl:text> let y = y1 + ua * (y2 - y1) + <xsl:text> return new DOMPoint(x,y); + <xsl:text>class ReferenceFrame { + <xsl:text> constructor( + <xsl:text> // [[Xminor,Xmajor], [Yminor,Ymajor]] + <xsl:text> // [Xlabel, Ylabel] + <xsl:text> // [Xline, Yline] + <xsl:text> // [Xformat, Yformat] printf-like formating strings + <xsl:text> this.axes = zip(labels,marks,lines,formats).map(args => new Axis(...args)); + <xsl:text> let [lx,ly] = this.axes.map(axis => axis.line); + <xsl:text> let [[xstart, xvect], [ystart, yvect]] = [lx,ly]; + <xsl:text> let base_point = this.getBasePoint(); + <xsl:text> // setup clipping for curves + <xsl:text> this.clipPathPathDattr = + <xsl:text> "m " + base_point.x + "," + base_point.y + " " + <xsl:text> + xvect.x + "," + xvect.y + " " + <xsl:text> + yvect.x + "," + yvect.y + " " + <xsl:text> + -xvect.x + "," + -xvect.y + " " + <xsl:text> + -yvect.x + "," + -yvect.y + " z"; + <xsl:text> this.base_ref = [base_point, xvect, yvect]; + <xsl:text> this.lengths = [xvect,yvect].map(v => vectorLength(v)); + <xsl:text> for(let axis of this.axes){ + <xsl:text> axis.setBasePoint(base_point); + <xsl:text> getLengths(){ + <xsl:text> return this.lengths; + <xsl:text> getBaseRef(){ + <xsl:text> return this.base_ref; + <xsl:text> getClipPathPathDattr(){ + <xsl:text> return this.clipPathPathDattr; + <xsl:text> applyRanges(ranges){ + <xsl:text> let origin_moves = zip(ranges,this.axes).map(([range,axis]) => axis.applyRange(...range)); + <xsl:text> zip(origin_moves.reverse(),this.axes).forEach(([vect,axis]) => axis.moveOrigin(vect)); + <xsl:text> getBasePoint() { + <xsl:text> let [[xstart, xvect], [ystart, yvect]] = this.axes.map(axis => axis.line); + <xsl:text> Compute graph clipping region base point + <xsl:text> ======================================== + <xsl:text> Clipping region is a parallelogram containing axes lines, + <xsl:text> and whose sides are parallel to axes line respectively. + <xsl:text> Given axes lines are not starting at the same point, hereafter is + <xsl:text> calculus of parallelogram base point. + <xsl:text> ^ given Y axis (yvect) + <xsl:text> xstart *---------*--------------> given X axis (xvect) + <xsl:text> *---------*-------------- + <xsl:text> base_point ystart + <xsl:text> let base_point = getLinesIntesection([xstart,yvect],[ystart,xvect]); + <xsl:text> return base_point; + <xsl:text> constructor(label, marks, line, format){ + <xsl:text> this.lineElement = line; + <xsl:text> this.line = lineFromPath(line); + <xsl:text> this.format = format; + <xsl:text> this.label = label; + <xsl:text> this.marks = marks; + <xsl:text> // add transforms for elements sliding along the axis line + <xsl:text> for(let [elementname,element] of zip(["minor", "major", "label"],[...marks,label])){ + <xsl:text> for(let name of ["base","slide"]){ + <xsl:text> let transform = svg_root.createSVGTransform(); + <xsl:text> element.transform.baseVal.insertItemBefore(transform,0); + <xsl:text> this[elementname+"_"+name+"_transform"]=transform; + <xsl:text> // group marks an labels together + <xsl:text> let parent = line.parentElement; + <xsl:text> this.marks_group = move_elements_to_group(marks); + <xsl:text> this.marks_and_label_group = move_elements_to_group([this.marks_group, label]); + <xsl:text> this.group = move_elements_to_group([this.marks_and_label_group,line]); + <xsl:text> parent.appendChild(this.group); + <xsl:text> // Add transforms to group + <xsl:text> for(let name of ["base","origin"]){ + <xsl:text> let transform = svg_root.createSVGTransform(); + <xsl:text> this.group.transform.baseVal.appendItem(transform); + <xsl:text> this[name+"_transform"]=transform; + <xsl:text> this.marks_and_label_group_transform = svg_root.createSVGTransform(); + <xsl:text> this.marks_and_label_group.transform.baseVal.appendItem(this.marks_and_label_group_transform); + <xsl:text> this.duplicates = []; + <xsl:text> this.last_duplicate_index = 0; + <xsl:text> setBasePoint(base_point){ + <xsl:text> // move Axis to base point + <xsl:text> let [start, _vect] = this.line; + <xsl:text> let v = vector(start, base_point); + <xsl:text> this.base_transform.setTranslate(v.x, v.y); + <xsl:text> // Move marks and label to base point. + <xsl:text> // _|_______ _|________ + <xsl:text> // | ' | ==> ' + <xsl:text> for(let [markname,mark] of zip(["minor", "major"],this.marks)){ + <xsl:text> let pos = vector( + <xsl:text> // Marks are expected to be paths + <xsl:text> // paths are expected to be lines + <xsl:text> // intersection with axis line is taken + <xsl:text> // as reference for mark position + <xsl:text> getLinesIntesection( + <xsl:text> this.line, lineFromPath(mark)),base_point); + <xsl:text> this[markname+"_base_transform"].setTranslate(pos.x - v.x, pos.y - v.y); + <xsl:text> if(markname == "major"){ // label follow major mark + <xsl:text> this.label_base_transform.setTranslate(pos.x - v.x, pos.y - v.y); + <xsl:text> moveOrigin(vect){ + <xsl:text> this.origin_transform.setTranslate(vect.x, vect.y); + <xsl:text> applyRange(min, max){ + <xsl:text> let range = max - min; + <xsl:text> // compute how many units for a mark + <xsl:text> // - Units are expected to be an order of magnitude smaller than range, + <xsl:text> // so that marks are not too dense and also not too sparse. + <xsl:text> // Order of magnitude of range is log10(range) + <xsl:text> // - Units are necessarily power of ten, otherwise it is complicated to + <xsl:text> // fill the text in labels... + <xsl:text> // Unit is pow(10, integer_number ) + <xsl:text> // - To transform order of magnitude to an integer, floor() is used. + <xsl:text> // This results in a count of mark fluctuating in between 10 and 100. + <xsl:text> // - To spare resources result is better in between 3 and 30, + <xsl:text> // and log10(3) is substracted to order of magnitude to obtain this + <xsl:text> let unit = Math.pow(10, Math.floor(Math.log10(range)-Math.log10(3))); + <xsl:text> // TODO: for time values (ms), units may be : + <xsl:text> // 1 -> ms + <xsl:text> // 10 -> s/100 + <xsl:text> // 100 -> s/10 + <xsl:text> // 1000 -> s + <xsl:text> // 60000 -> min + <xsl:text> // 3600000 -> hour + <xsl:text> // Compute position of origin along axis [0...range] + <xsl:text> // min < 0, max > 0, offset = -min + <xsl:text> // _____________|________________ + <xsl:text> // ... -3 -2 -1 |0 1 2 3 4 ... + <xsl:text> // <--offset---> ^ + <xsl:text> // |_original + <xsl:text> // min > 0, max > 0, offset = 0 + <xsl:text> // |________________ + <xsl:text> // |6 7 8 9 10... + <xsl:text> // |_original + <xsl:text> // min < 0, max < 0, offset = max-min (range) + <xsl:text> // _____________|_ + <xsl:text> // ... -5 -4 -3 |-2 + <xsl:text> // <--offset---> ^ + <xsl:text> // |_original + <xsl:text> let offset = (max>=0 && min>=0) ? 0 : ( + <xsl:text> (max<0 && min<0) ? range : -min); + <xsl:text> // compute unit vector + <xsl:text> let [_start, vect] = this.line; + <xsl:text> let unit_vect = vectorscale(vect, 1/range); + <xsl:text> let [mark_min, mark_max, mark_offset] = [min,max,offset].map(val => Math.round(val/unit)); + <xsl:text> let mark_count = mark_max-mark_min; + <xsl:text> // apply unit vector to marks and label + <xsl:text> // offset is a representing position of an + <xsl:text> // axis along the opposit axis line, expressed in major marks units + <xsl:text> // unit_vect is unit vector + <xsl:text> // | unit_vect + <xsl:text> // |<---> + <xsl:text> // _________|__________> + <xsl:text> // ^ | ' | ' | ' + <xsl:text> // |yoffset | 1 + <xsl:text> // v xoffset| + <xsl:text> // X<------>| + <xsl:text> // base_point + <xsl:text> // move major marks and label to first positive mark position + <xsl:text> // let v = vectorscale(unit_vect, unit); + <xsl:text> // this.label_slide_transform.setTranslate(v.x, v.y); + <xsl:text> // this.major_slide_transform.setTranslate(v.x, v.y); + <xsl:text> // move minor mark to first half positive mark position + <xsl:text> let v = vectorscale(unit_vect, unit/2); + <xsl:text> this.minor_slide_transform.setTranslate(v.x, v.y); + <xsl:text> // duplicate marks and labels as needed + <xsl:text> let current_mark_count = this.duplicates.length; + <xsl:text> for(let i = current_mark_count; i <= mark_count; i++){ + <xsl:text> // cloneNode() label and add a svg:use of marks in a new group + <xsl:text> let newgroup = document.createElementNS(xmlns,"g"); + <xsl:text> let transform = svg_root.createSVGTransform(); + <xsl:text> let newlabel = this.label.cloneNode(true); + <xsl:text> let newuse = document.createElementNS(xmlns,"use"); + <xsl:text> let newuseAttr = document.createAttribute("href"); + <xsl:text> newuseAttr.value = "#"+this.marks_group.id; + <xsl:text> newuse.setAttributeNode(newuseAttr); + <xsl:text> newgroup.transform.baseVal.appendItem(transform); + <xsl:text> newgroup.appendChild(newlabel); + <xsl:text> newgroup.appendChild(newuse); + <xsl:text> this.duplicates.push([transform,newgroup]); + <xsl:text> // move marks and labels, set labels + <xsl:text> // min > 0, max > 0, offset = 0 + <xsl:text> // |________> + <xsl:text> // base_point + <xsl:text> // min < 0, max > 0, offset = -min + <xsl:text> // _________|__________> + <xsl:text> // ' | ' | ' | ' + <xsl:text> // X<------>| + <xsl:text> // base_point + <xsl:text> // min < 0, max < 0, offset = range + <xsl:text> // ____________| + <xsl:text> // ' | ' | |' + <xsl:text> // X<--------->| + <xsl:text> // base_point + <xsl:text> let duplicate_index = 0; + <xsl:text> for(let mark_index = 0; mark_index <= mark_count; mark_index++){ + <xsl:text> let val = (mark_min + mark_index) * unit; + <xsl:text> let vec = vectorscale(unit_vect, val - min); + <xsl:text> let text = this.format ? sprintf(this.format, val) : val.toString(); + <xsl:text> if(mark_index == mark_offset){ + <xsl:text> // apply offset to original marks and label groups + <xsl:text> this.marks_and_label_group_transform.setTranslate(vec.x, vec.y); + <xsl:text> // update original label text + <xsl:text> this.label.getElementsByTagName("tspan")[0].textContent = text; + <xsl:text> let [transform,element] = this.duplicates[duplicate_index++]; + <xsl:text> // apply unit vector*N to marks and label groups + <xsl:text> transform.setTranslate(vec.x, vec.y); + <xsl:text> // update label text + <xsl:text> element.getElementsByTagName("tspan")[0].textContent = text; + <xsl:text> // Attach to group if not already + <xsl:text> if(element.parentElement == null){ + <xsl:text> this.group.appendChild(element); + <xsl:text> let save_duplicate_index = duplicate_index; + <xsl:text> // dettach marks and label from group if not anymore visible + <xsl:text> for(;duplicate_index < this.last_duplicate_index; duplicate_index++){ + <xsl:text> let [transform,element] = this.duplicates[duplicate_index]; + <xsl:text> this.group.removeChild(element); + <xsl:text> this.last_duplicate_index = save_duplicate_index; + <xsl:text> return vectorscale(unit_vect, offset); + <xsl:template match="widget[@type='Swipe']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Swipe widget detects left, right, up and down swiping motion and executes + <xsl:text>associated actions. + <xsl:text>For each of the motions to be detected there must exist a group inside the + <xsl:text>widget with the label "dir_{direction}" where {direction} is from the set: + <xsl:text>left, right, up, down. + <xsl:text>Inside each such group, there should be an element with the label + <xsl:text>"thresholds:{x},{y}", where {x} and {y} represent pixel count along X and Y + <xsl:text>axis that define the motion needed to be labelled as swipe. + <xsl:text>Alongside that, one can add up to one element with the label + <xsl:text>"jump:{pagename}", where {pagename} is the name of the page which is the + <xsl:text>target of a jump. + <xsl:text>There can also be any number of elements with the label + <xsl:text>"{expression}:{variable}", where {expression} is a mathematical expression + <xsl:text>to be applied to {variable}. All variables that appear in such elements + <xsl:text>must also be listed as paths in the widget label. + <xsl:text>HMI:Swipe@/VAR0@/VAR1 + <xsl:text> - thresholds:-100,20 + <xsl:text> - thresholds:20,100 + <xsl:text>This detects left and up swipe motion. To detect swipe left, movement must + <xsl:text>be at least 100 pixels to the left and at most 20 pixels up or down. If + <xsl:text>detected, it will increase VAR0 by 1 and jump to a page named Home. To + <xsl:text>detect swipe up, movement must be at most 20 pixels left or right, and at + <xsl:text>least 100 pixels up. If detected, it will decrease VAR0 by 1 and set VAR1 + <xsl:text>Detect swipe motion and react accordingly</xsl:text> + <path name="variable" count="optional" accepts="all"/> + <xsl:template match="widget[@type='Swipe']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>SwipeWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 2; + <xsl:text> startX = -1; + <xsl:text> startY = -1; + <xsl:text> settings = { + <xsl:text> actions: [], + <xsl:text> x_threshold: Number.MIN_SAFE_INTEGER, + <xsl:text> y_threshold: 0 + <xsl:text> actions: [], + <xsl:text> x_threshold: Number.MAX_SAFE_INTEGER, + <xsl:text> y_threshold: 0 + <xsl:text> actions: [], + <xsl:text> x_threshold: 0, + <xsl:text> y_threshold: Number.MIN_SAFE_INTEGER + <xsl:text> actions: [], + <xsl:text> x_threshold: 0, + <xsl:text> y_threshold: Number.MAX_SAFE_INTEGER + <xsl:text> onmouseup(evt) { + <xsl:text> var xDiff = evt.pageX - this.startX; + <xsl:text> var yDiff = evt.pageY - this.startY; + <xsl:text> svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); + <xsl:text> var action = null; + <xsl:text> if (xDiff <= this.settings.left.x_threshold && Math.abs(yDiff) < this.settings.left.y_threshold) { + <xsl:text> action = "left"; + <xsl:text> } else if (xDiff >= this.settings.right.x_threshold && Math.abs(yDiff) < this.settings.right.y_threshold) { + <xsl:text> action = "right"; + <xsl:text> } else if (yDiff <= this.settings.up.y_threshold && Math.abs(xDiff) < this.settings.up.x_threshold) { + <xsl:text> action = "up"; + <xsl:text> } else if (yDiff >= this.settings.down.y_threshold && Math.abs(xDiff) < this.settings.down.x_threshold) { + <xsl:text> action = "down"; + <xsl:text> if (action) { + <xsl:text> for (var a of this.settings[action].actions) { + <xsl:text> if (a.action == "jump") + <xsl:text> fading_page_switch(a.target); + <xsl:text> else if (a.action == "change") { + <xsl:text> this.change_hmi_value(a.var_idx, a.change); + <xsl:text> onmousedown(evt) { + <xsl:text> this.startX = evt.pageX; + <xsl:text> this.startY = evt.pageY; + <xsl:text> svg_root.addEventListener("pointerup", this.bound_onmouseup, true); + <xsl:text> this.request_animate(); + <xsl:template match="widget[@type='Swipe']" 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 name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:text> init: function() { + <xsl:text> this.bound_onmouseup = this.onmouseup.bind(this); + <xsl:text> this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); + <xsl:for-each select="$hmi_element/*[starts-with(@inkscape:label,'dir_')]"> + <xsl:variable name="name" select="@inkscape:label"/> + <xsl:variable name="dir" select="substring-after(@inkscape:label,'_')"/> + <xsl:for-each select="$hmi_element/*[@inkscape:label = $name]/descendant::svg:*"> + <xsl:if test="starts-with(@inkscape:label,'thresholds:')"> + <xsl:variable name="values" select="substring-after(@inkscape:label,':')"/> + <xsl:variable name="threshx" select="substring-before($values,',')"/> + <xsl:variable name="threshy" select="substring-after($values,',')"/> + <xsl:text> this.settings["</xsl:text> + <xsl:value-of select="$dir"/> + <xsl:text>"].x_threshold = </xsl:text> + <xsl:value-of select="$threshx"/> + <xsl:text> this.settings["</xsl:text> + <xsl:value-of select="$dir"/> + <xsl:text>"].y_threshold = </xsl:text> + <xsl:value-of select="$threshy"/> + <xsl:if test="starts-with(@inkscape:label,'jump:')"> + <xsl:variable name="target" select="substring-after(@inkscape:label,':')"/> + <xsl:text> this.settings["</xsl:text> + <xsl:value-of select="$dir"/> + <xsl:text>"].actions.push({ + <xsl:text> action: "jump", + <xsl:text> target: "</xsl:text> + <xsl:value-of select="$target"/> + <xsl:if test="regexp:test(@inkscape:label,'^[=+\-].+')"> + <xsl:variable name="var" select="substring-after(@inkscape:label,'@')"/> + <xsl:variable name="change" select="substring-before(@inkscape:label,'@')"/> + <xsl:text> this.settings["</xsl:text> + <xsl:value-of select="$dir"/> + <xsl:text>"].actions.push({ + <xsl:text> action: "change", + <xsl:text> var_idx: this.var_indices["</xsl:text> + <xsl:value-of select="$var"/> + <xsl:text> change: "</xsl:text> + <xsl:value-of select="$change"/> + <xsl:template match="/"> + <xsl:text>Made with SVGHMI. https://beremiz.org</xsl:text> + <html xmlns="http://www.w3.org/1999/xhtml" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <style type="text/css" media="screen"> + <xsl:value-of select="ns:GetFonts()"/> + <xsl:apply-templates select="document('')/*/cssdefs:*"/> + <body style="margin:0;overflow:hidden;user-select:none;touch-action:none;"> + <xsl:copy-of select="$result_svg"/> + <xsl:text>From https://github.com/keyvan-m-sadeghi/pythonic + <xsl:text>Slightly modified in order to be usable in browser (i.e. not as a node.js module) + <xsl:text>The MIT License (MIT) + <xsl:text>Copyright (c) 2016 Assister.Ai + <xsl:text>Permission is hereby granted, free of charge, to any person obtaining a copy of + <xsl:text>this software and associated documentation files (the "Software"), to deal in + <xsl:text>the Software without restriction, including without limitation the rights to + <xsl:text>use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + <xsl:text>the Software, and to permit persons to whom the Software is furnished to do so, + <xsl:text>subject to the following conditions: + <xsl:text>The above copyright notice and this permission notice shall be included in all + <xsl:text>copies or substantial portions of the Software. + <xsl:text>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + <xsl:text>IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + <xsl:text>FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + <xsl:text>COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + <xsl:text>IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + <xsl:text>CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + <xsl:text>class Iterator { + <xsl:text> constructor(generator) { + <xsl:text> this[Symbol.iterator] = generator; + <xsl:text> async * [Symbol.asyncIterator]() { + <xsl:text> for (const element of this) { + <xsl:text> yield await element; + <xsl:text> forEach(callback) { + <xsl:text> for (const element of this) { + <xsl:text> callback(element); + <xsl:text> map(callback) { + <xsl:text> const result = []; + <xsl:text> for (const element of this) { + <xsl:text> result.push(callback(element)); + <xsl:text> return result; + <xsl:text> filter(callback) { + <xsl:text> const result = []; + <xsl:text> for (const element of this) { + <xsl:text> if (callback(element)) { + <xsl:text> result.push(element); + <xsl:text> return result; + <xsl:text> reduce(callback, initialValue) { + <xsl:text> let empty = typeof initialValue === 'undefined'; + <xsl:text> let accumulator = initialValue; + <xsl:text> let index = 0; + <xsl:text> for (const currentValue of this) { + <xsl:text> if (empty) { + <xsl:text> accumulator = currentValue; + <xsl:text> empty = false; + <xsl:text> accumulator = callback(accumulator, currentValue, index, this); + <xsl:text> if (empty) { + <xsl:text> throw new TypeError('Reduce of empty Iterator with no initial value'); + <xsl:text> return accumulator; + <xsl:text> some(callback) { + <xsl:text> for (const element of this) { + <xsl:text> if (callback(element)) { + <xsl:text> return true; + <xsl:text> return false; + <xsl:text> every(callback) { + <xsl:text> for (const element of this) { + <xsl:text> if (!callback(element)) { + <xsl:text> return false; + <xsl:text> return true; + <xsl:text> static fromIterable(iterable) { + <xsl:text> return new Iterator(function * () { + <xsl:text> for (const element of iterable) { + <xsl:text> yield element; + <xsl:text> return Array.from(this); + <xsl:text> if (!this.currentInvokedGenerator) { + <xsl:text> this.currentInvokedGenerator = this[Symbol.iterator](); + <xsl:text> return this.currentInvokedGenerator.next(); + <xsl:text> delete this.currentInvokedGenerator; + <xsl:text>function rangeSimple(stop) { + <xsl:text> return new Iterator(function * () { + <xsl:text> for (let i = 0; i < stop; i++) { + <xsl:text>function rangeOverload(start, stop, step = 1) { + <xsl:text> return new Iterator(function * () { + <xsl:text> for (let i = start; i < stop; i += step) { + <xsl:text>function range(...args) { + <xsl:text> if (args.length < 2) { + <xsl:text> return rangeSimple(...args); + <xsl:text> return rangeOverload(...args); + <xsl:text>function enumerate(iterable) { + <xsl:text> return new Iterator(function * () { + <xsl:text> let index = 0; + <xsl:text> for (const element of iterable) { + <xsl:text> yield [index, element]; + <xsl:text>const _zip = longest => (...iterables) => { + <xsl:text> if (iterables.length == 0) { + <xsl:text> // works starting with 1 iterable + <xsl:text> // [a,b,c] -> [[a],[b],[c]] + <xsl:text> // [a,b,c],[d,e,f] -> [[a,d],[b,e],[c,f]] + <xsl:text> throw new TypeError("zip takes 1 iterables at least, "+iterables.length+" given"); + <xsl:text> return new Iterator(function * () { + <xsl:text> const iterators = iterables.map(iterable => Iterator.fromIterable(iterable)); + <xsl:text> while (true) { + <xsl:text> const row = iterators.map(iterator => iterator.next()); + <xsl:text> const check = longest ? row.every.bind(row) : row.some.bind(row); + <xsl:text> if (check(next => next.done)) { + <xsl:text> yield row.map(next => next.value); + <xsl:text>const zip = _zip(false), zipLongest= _zip(true); + <xsl:text>function items(obj) { + <xsl:text> let {keys, get} = obj; + <xsl:text> if (obj instanceof Map) { + <xsl:text> keys = keys.bind(obj); + <xsl:text> get = get.bind(obj); + <xsl:text> keys = function () { + <xsl:text> return Object.keys(obj); + <xsl:text> get = function (key) { + <xsl:text> return obj[key]; + <xsl:text> return new Iterator(function * () { + <xsl:text> for (const key of keys()) { + <xsl:text> yield [key, get(key)]; + <xsl:text>module.exports = {Iterator, range, enumerate, zip: _zip(false), zipLongest: _zip(true), items}; +// Early independent declarations + <xsl:apply-templates select="document('')/*/preamble:*"/> +// Declarations depending on preamble + <xsl:apply-templates select="document('')/*/declarations:*"/> +// Order independent declaration and code + <xsl:apply-templates select="document('')/*/definitions:*"/> +// Statements that needs to be at the end + <xsl:apply-templates select="document('')/*/epilogue:*"/> + <xsl:text>/* https://github.com/alexei/sprintf.js/blob/master/src/sprintf.js */ + <xsl:text>/* global window, exports, define */ + <xsl:text>!function() { + <xsl:text> 'use strict' + <xsl:text> not_string: /[^s]/, + <xsl:text> not_bool: /[^t]/, + <xsl:text> not_type: /[^T]/, + <xsl:text> not_primitive: /[^v]/, + <xsl:text> number: /[diefg]/, + <xsl:text> numeric_arg: /[bcdiefguxX]/, + <xsl:text> json: /[j]/, + <xsl:text> not_json: /[^j]/, + <xsl:text> text: /^[^%]+/, + <xsl:text> modulo: /^%{2}/, + <xsl:text> placeholder: /^%(?:([1-9]\d*)\$|\(([^)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijostTuvxXD])/, + <xsl:text> key: /^([a-z_][a-z_\d]*)/i, + <xsl:text> key_access: /^\.([a-z_][a-z_\d]*)/i, + <xsl:text> index_access: /^\[(\d+)\]/, + <xsl:text> sign: /^[+-]/ + <xsl:text> function sprintf(key) { + <xsl:text> // arguments is not an array, but should be fine for this call + <xsl:text> return sprintf_format(sprintf_parse(key), arguments) + <xsl:text> function vsprintf(fmt, argv) { + <xsl:text> return sprintf.apply(null, [fmt].concat(argv || [])) + <xsl:text> function sprintf_format(parse_tree, argv) { + <xsl:text> var cursor = 1, tree_length = parse_tree.length, arg, output = '', i, k, ph, pad, pad_character, pad_length, is_positive, sign + <xsl:text> for (i = 0; i < tree_length; i++) { + <xsl:text> if (typeof parse_tree[i] === 'string') { + <xsl:text> output += parse_tree[i] + <xsl:text> else if (typeof parse_tree[i] === 'object') { + <xsl:text> ph = parse_tree[i] // convenience purposes only + <xsl:text> if (ph.keys) { // keyword argument + <xsl:text> arg = argv[cursor] + <xsl:text> for (k = 0; k < ph.keys.length; k++) { + <xsl:text> if (arg == undefined) { + <xsl:text> throw new Error(sprintf('[sprintf] Cannot access property "%s" of undefined value "%s"', ph.keys[k], ph.keys[k-1])) + <xsl:text> arg = arg[ph.keys[k]] + <xsl:text> else if (ph.param_no) { // positional argument (explicit) + <xsl:text> arg = argv[ph.param_no] + <xsl:text> else { // positional argument (implicit) + <xsl:text> arg = argv[cursor++] + <xsl:text> if (re.not_type.test(ph.type) && re.not_primitive.test(ph.type) && arg instanceof Function) { + <xsl:text> if (re.numeric_arg.test(ph.type)){ + <xsl:text> let argtype = typeof arg; + <xsl:text> if ( argtype !== 'bigint') { + <xsl:text> if ( argtype !== 'number' && isNaN(arg) ) { + <xsl:text> throw new TypeError(sprintf('[sprintf] expecting number but found %T', arg)) + <xsl:text> if (re.number.test(ph.type)) { + <xsl:text> is_positive = arg >= 0 + <xsl:text> switch (ph.type) { + <xsl:text> arg = parseInt(arg, 10).toString(2) + <xsl:text> arg = String.fromCharCode(parseInt(arg, 10)) + <xsl:text> arg = parseInt(arg, 10) + <xsl:text> select date format with width + <xsl:text> select time format with precision + <xsl:text> %D => 13:31 AM (default) + <xsl:text> %1D => 13:31 AM + <xsl:text> %.1D => 07/07/20 + <xsl:text> %1.1D => 07/07/20, 13:31 AM + <xsl:text> %1.2D => 07/07/20, 13:31:55 AM + <xsl:text> %2.2D => May 5, 2022, 9:29:16 AM + <xsl:text> %3.3D => May 5, 2022 at 9:28:16 AM GMT+2 + <xsl:text> %4.4D => Thursday, May 5, 2022 at 9:26:59 AM Central European Summer Time + <xsl:text> see meaning of DateTimeFormat's options "datestyle" and "timestyle" in MDN + <xsl:text> let [datestyle, timestyle] = [ph.width, ph.precision].map(val => ({ + <xsl:text> 2: "medium", + <xsl:text> if(timestyle === undefined && datestyle === undefined){ + <xsl:text> timestyle = "short"; + <xsl:text> let options = { + <xsl:text> dateStyle: datestyle, + <xsl:text> timeStyle: timestyle, + <xsl:text> hour12: false + <xsl:text> /* get lang from globals */ + <xsl:text> let lang = get_current_lang_code(); + <xsl:text> f = new Intl.DateTimeFormat(lang, options); + <xsl:text> } catch(e) { + <xsl:text> f = new Intl.DateTimeFormat('en-US', options); + <xsl:text> arg = f.format(arg); + <xsl:text> TODO: select with padding char + <xsl:text> a: absolute time and date (default) + <xsl:text> r: relative time + <xsl:text> arg = JSON.stringify(arg, null, ph.width ? parseInt(ph.width) : 0) + <xsl:text> arg = ph.precision ? parseFloat(arg).toExponential(ph.precision) : parseFloat(arg).toExponential() + <xsl:text> arg = ph.precision ? parseFloat(arg).toFixed(ph.precision) : parseFloat(arg) + <xsl:text> arg = ph.precision ? String(Number(arg.toPrecision(ph.precision))) : parseFloat(arg) + <xsl:text> arg = (parseInt(arg, 10) >>> 0).toString(8) + <xsl:text> arg = String(arg) + <xsl:text> arg = (ph.precision ? arg.substring(0, ph.precision) : arg) + <xsl:text> arg = String(!!arg) + <xsl:text> arg = (ph.precision ? arg.substring(0, ph.precision) : arg) + <xsl:text> arg = Object.prototype.toString.call(arg).slice(8, -1).toLowerCase() + <xsl:text> arg = (ph.precision ? arg.substring(0, ph.precision) : arg) + <xsl:text> arg = parseInt(arg, 10) >>> 0 + <xsl:text> arg = arg.valueOf() + <xsl:text> arg = (ph.precision ? arg.substring(0, ph.precision) : arg) + <xsl:text> arg = (parseInt(arg, 10) >>> 0).toString(16) + <xsl:text> arg = (parseInt(arg, 10) >>> 0).toString(16).toUpperCase() + <xsl:text> if (re.json.test(ph.type)) { + <xsl:text> output += arg + <xsl:text> if (re.number.test(ph.type) && (!is_positive || ph.sign)) { + <xsl:text> sign = is_positive ? '+' : '-' + <xsl:text> arg = arg.toString().replace(re.sign, '') + <xsl:text> pad_character = ph.pad_char ? ph.pad_char === '0' ? '0' : ph.pad_char.charAt(1) : ' ' + <xsl:text> pad_length = ph.width - (sign + arg).length + <xsl:text> pad = ph.width ? (pad_length > 0 ? pad_character.repeat(pad_length) : '') : '' + <xsl:text> output += ph.align ? sign + arg + pad : (pad_character === '0' ? sign + pad + arg : pad + sign + arg) + <xsl:text> return output + <xsl:text> var sprintf_cache = Object.create(null) + <xsl:text> function sprintf_parse(fmt) { + <xsl:text> if (sprintf_cache[fmt]) { + <xsl:text> return sprintf_cache[fmt] + <xsl:text> var _fmt = fmt, match, parse_tree = [], arg_names = 0 + <xsl:text> while (_fmt) { + <xsl:text> if ((match = re.text.exec(_fmt)) !== null) { + <xsl:text> parse_tree.push(match[0]) + <xsl:text> else if ((match = re.modulo.exec(_fmt)) !== null) { + <xsl:text> parse_tree.push('%') + <xsl:text> else if ((match = re.placeholder.exec(_fmt)) !== null) { + <xsl:text> if (match[2]) { + <xsl:text> arg_names |= 1 + <xsl:text> var field_list = [], replacement_field = match[2], field_match = [] + <xsl:text> if ((field_match = re.key.exec(replacement_field)) !== null) { + <xsl:text> field_list.push(field_match[1]) + <xsl:text> while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') { + <xsl:text> if ((field_match = re.key_access.exec(replacement_field)) !== null) { + <xsl:text> field_list.push(field_match[1]) + <xsl:text> else if ((field_match = re.index_access.exec(replacement_field)) !== null) { + <xsl:text> field_list.push(field_match[1]) + <xsl:text> throw new SyntaxError('[sprintf] failed to parse named argument key') + <xsl:text> throw new SyntaxError('[sprintf] failed to parse named argument key') + <xsl:text> match[2] = field_list + <xsl:text> arg_names |= 2 + <xsl:text> if (arg_names === 3) { + <xsl:text> throw new Error('[sprintf] mixing positional and named placeholders is not (yet) supported') + <xsl:text> parse_tree.push( + <xsl:text> placeholder: match[0], + <xsl:text> param_no: match[1], + <xsl:text> keys: match[2], + <xsl:text> sign: match[3], + <xsl:text> pad_char: match[4], + <xsl:text> align: match[5], + <xsl:text> width: match[6], + <xsl:text> precision: match[7], + <xsl:text> type: match[8] + <xsl:text> throw new SyntaxError('[sprintf] unexpected placeholder') + <xsl:text> _fmt = _fmt.substring(match[0].length) + <xsl:text> return sprintf_cache[fmt] = parse_tree + <xsl:text> * export to either browser or node.js + <xsl:text> /* eslint-disable quote-props */ + <xsl:text> if (typeof exports !== 'undefined') { + <xsl:text> exports['sprintf'] = sprintf + <xsl:text> exports['vsprintf'] = vsprintf + <xsl:text> if (typeof window !== 'undefined') { + <xsl:text> window['sprintf'] = sprintf + <xsl:text> window['vsprintf'] = vsprintf + <xsl:text> if (typeof define === 'function' && define['amd']) { + <xsl:text> define(function() { + <xsl:text> 'sprintf': sprintf, + <xsl:text> 'vsprintf': vsprintf + <xsl:text> /* eslint-enable quote-props */ + <xsl:text>}(); // eslint-disable-line + <xsl:text>function dispatch_value(index, value) { + <xsl:text> let widgets = subscribers(index); + <xsl:text> let oldval = cache[index]; + <xsl:text> cache[index] = value; + <xsl:text> if(widgets.size > 0) { + <xsl:text> for(let widget of widgets){ + <xsl:text> widget.new_hmi_value(index, value, oldval); + <xsl:text>function init_widgets() { + <xsl:text> Object.keys(hmi_widgets).forEach(function(id) { + <xsl:text> let widget = hmi_widgets[id]; + <xsl:text> if(widget.curr_value != undefined) return; + <xsl:text> widget.do_init(); + <xsl:text>// Open WebSocket to relative "/ws" address + <xsl:text>var has_watchdog = window.location.hash == "#watchdog"; + <xsl:text>const dvgetters = { + <xsl:text> SINT: (dv,offset) => [dv.getInt8(offset, true), 1], + <xsl:text> INT: (dv,offset) => [dv.getInt16(offset, true), 2], + <xsl:text> DINT: (dv,offset) => [dv.getInt32(offset, true), 4], + <xsl:text> LINT: (dv,offset) => [dv.getBigInt64(offset, true), 8], + <xsl:text> USINT: (dv,offset) => [dv.getUint8(offset, true), 1], + <xsl:text> UINT: (dv,offset) => [dv.getUint16(offset, true), 2], + <xsl:text> UDINT: (dv,offset) => [dv.getUint32(offset, true), 4], + <xsl:text> ULINT: (dv,offset) => [dv.getBigUint64(offset, true), 8], + <xsl:text> BOOL: (dv,offset) => [dv.getInt8(offset, true), 1], + <xsl:text> NODE: (dv,offset) => [dv.getInt8(offset, true), 1], + <xsl:text> REAL: (dv,offset) => [dv.getFloat32(offset, true), 4], + <xsl:text> STRING: (dv, offset) => { + <xsl:text> const size = dv.getInt8(offset); + <xsl:text> String.fromCharCode.apply(null, new Uint8Array( + <xsl:text> dv.buffer, /* original buffer */ + <xsl:text> offset + 1, /* string starts after size*/ + <xsl:text> size /* size of string */ + <xsl:text> )), size + 1]; /* total increment */ + <xsl:text>// Called on requestAnimationFrame, modifies DOM + <xsl:text>var requestAnimationFrameID = null; + <xsl:text>function animate() { + <xsl:text> let rearm = true; + <xsl:text> if(page_fading == "pending" || page_fading == "forced"){ + <xsl:text> if(page_fading == "pending") + <xsl:text> svg_root.classList.add("fade-out-page"); + <xsl:text> page_fading = "in_progress"; + <xsl:text> if(page_fading_args.length) + <xsl:text> setTimeout(function(){ + <xsl:text> switch_page(...page_fading_args); + <xsl:text> // Do the page swith if pending + <xsl:text> if(page_switch_in_progress){ + <xsl:text> if(current_subscribed_page != current_visible_page){ + <xsl:text> switch_visible_page(current_subscribed_page); + <xsl:text> page_switch_in_progress = false; + <xsl:text> if(page_fading == "in_progress"){ + <xsl:text> svg_root.classList.remove("fade-out-page"); + <xsl:text> page_fading = "off"; + <xsl:text> if(jumps_need_update) update_jumps(); + <xsl:text> pending_widget_animates.forEach(widget => widget._animate()); + <xsl:text> pending_widget_animates = []; + <xsl:text> rearm = false; + <xsl:text> requestAnimationFrameID = null; + <xsl:text> if(rearm) requestHMIAnimation(); + <xsl:text>function requestHMIAnimation() { + <xsl:text> if(requestAnimationFrameID == null){ + <xsl:text> requestAnimationFrameID = window.requestAnimationFrame(animate); + <xsl:text>// Message reception handler + <xsl:text>// Hash is verified and HMI values updates resulting from binary parsing + <xsl:text>// are stored until browser can compute next frame, DOM is left untouched + <xsl:text>function ws_onmessage(evt) { + <xsl:text> let data = evt.data; + <xsl:text> let dv = new DataView(data); + <xsl:text> for(let hash_int of hmi_hash) { + <xsl:text> if(hash_int != dv.getUint8(i)){ + <xsl:text> throw new Error("Hash doesn't match"); + <xsl:text> while(i < data.byteLength){ + <xsl:text> let index = dv.getUint32(i, true); + <xsl:text> let iectype = hmitree_types[index]; + <xsl:text> if(iectype != undefined){ + <xsl:text> let dvgetter = dvgetters[iectype]; + <xsl:text> let [value, bytesize] = dvgetter(dv,i); + <xsl:text> dispatch_value(index, value); + <xsl:text> i += bytesize; + <xsl:text> throw new Error("Unknown index "+index); + <xsl:text> // register for rendering on next frame, since there are updates + <xsl:text> } catch(err) { + <xsl:text> // 1003 is for "Unsupported Data" + <xsl:text> // ws.close(1003, err.message); + <xsl:text> // TODO : remove debug alert ? + <xsl:text> alert("Error : "+err.message+"\nHMI will be reloaded."); + <xsl:text> // force reload ignoring cache + <xsl:text> location.reload(true); + <xsl:text>hmi_hash_u8 = new Uint8Array(hmi_hash); + <xsl:text>var ws = null; + <xsl:text>function send_blob(data) { + <xsl:text> if(data.length > 0 && ws && ws.readyState == WebSocket.OPEN) { + <xsl:text> ws.send(new Blob([hmi_hash_u8].concat(data))); + <xsl:text>const typedarray_types = { + <xsl:text> SINT: (number) => new Int8Array([number]), + <xsl:text> INT: (number) => new Int16Array([number]), + <xsl:text> DINT: (number) => new Int32Array([number]), + <xsl:text> LINT: (number) => new Int64Array([number]), + <xsl:text> USINT: (number) => new Uint8Array([number]), + <xsl:text> UINT: (number) => new Uint16Array([number]), + <xsl:text> UDINT: (number) => new Uint32Array([number]), + <xsl:text> ULINT: (number) => new Uint64Array([number]), + <xsl:text> BOOL: (truth) => new Int8Array([truth]), + <xsl:text> NODE: (truth) => new Int8Array([truth]), + <xsl:text> REAL: (number) => new Float32Array([number]), + <xsl:text> STRING: (str) => { + <xsl:text> // beremiz default string max size is 128 + <xsl:text> str = str.slice(0,128); + <xsl:text> binary = new Uint8Array(str.length + 1); + <xsl:text> binary[0] = str.length; + <xsl:text> for(let i = 0; i < str.length; i++){ + <xsl:text> binary[i+1] = str.charCodeAt(i); + <xsl:text> return binary; + <xsl:text>function send_reset() { + <xsl:text> send_blob(new Uint8Array([1])); /* reset = 1 */ + <xsl:text>var subscriptions = []; + <xsl:text>var subscriptions_update_requested = false; + <xsl:text>function subscribers(index) { + <xsl:text> let entry = subscriptions[index]; + <xsl:text> if(entry == undefined){ + <xsl:text> res = new Set(); + <xsl:text> subscriptions[index] = [res,0]; + <xsl:text> [res, _ign] = entry; + <xsl:text>function get_subscription_period(index) { + <xsl:text> let entry = subscriptions[index]; + <xsl:text> if(entry == undefined) + <xsl:text> let [_ign, period] = entry; + <xsl:text> return period; + <xsl:text>function set_subscription_period(index, period) { + <xsl:text> let entry = subscriptions[index]; + <xsl:text> if(entry == undefined){ + <xsl:text> subscriptions[index] = [new Set(), period]; + <xsl:text> entry[1] = period; + <xsl:text>function reset_subscription_periods() { + <xsl:text> for(let index in subscriptions) + <xsl:text> subscriptions[index][1] = 0; + <xsl:text>if(has_watchdog){ + <xsl:text> // artificially subscribe the watchdog widget to "/heartbeat" hmi variable + <xsl:text> // Since dispatch directly calls change_hmi_value, + <xsl:text> // PLC will periodically send variable at given frequency + <xsl:text> subscribers(heartbeat_index).add({ + <xsl:text> /* type: "Watchdog", */ + <xsl:text> frequency: 1, + <xsl:text> indexes: [heartbeat_index], + <xsl:text> new_hmi_value: function(index, value, oldval) { + <xsl:text> apply_hmi_value(heartbeat_index, value+1); + <xsl:text>var page_fading = "off"; + <xsl:text>var page_fading_args = "off"; + <xsl:text>function fading_page_switch(...args){ + <xsl:text> if(page_fading == "in_progress") + <xsl:text> page_fading = "forced"; + <xsl:text> page_fading = "pending"; + <xsl:text> page_fading_args = args; + <xsl:text> requestHMIAnimation(); + <xsl:text>document.body.style.backgroundColor = "black"; + <xsl:text>// subscribe to per instance current page hmi variable + <xsl:text>// PLC must prefix page name with "!" for page switch to happen + <xsl:text>subscribers(current_page_var_index).add({ + <xsl:text> frequency: 1, + <xsl:text> indexes: [current_page_var_index], + <xsl:text> new_hmi_value: function(index, value, oldval) { + <xsl:text> if(value.startsWith("!")) + <xsl:text> fading_page_switch(value.slice(1)); + <xsl:text>function svg_text_to_multiline(elt) { + <xsl:text> return(Array.prototype.map.call(elt.children, x=>x.textContent).join("\n")); + <xsl:text>function multiline_to_svg_text(elt, str, blank) { + <xsl:text> str.split('\n').map((line,i) => {elt.children[i].textContent = blank?"":line;}); + <xsl:text>function switch_langnum(langnum) { + <xsl:text> langnum = Math.max(0, Math.min(langs.length - 1, langnum)); + <xsl:text> for (let translation of translations) { + <xsl:text> let [objs, msgs] = translation; + <xsl:text> let msg = msgs[langnum]; + <xsl:text> for (let obj of objs) { + <xsl:text> multiline_to_svg_text(obj, msg); + <xsl:text> obj.setAttribute("lang",langnum); + <xsl:text> return langnum; + <xsl:text>// backup original texts + <xsl:text>for (let translation of translations) { + <xsl:text> let [objs, msgs] = translation; + <xsl:text> msgs.unshift(svg_text_to_multiline(objs[0])); + <xsl:text>var lang_local_index = hmi_local_index("lang"); + <xsl:text>var langcode_local_index = hmi_local_index("lang_code"); + <xsl:text>var langname_local_index = hmi_local_index("lang_name"); + <xsl:text>subscribers(lang_local_index).add({ + <xsl:text> indexes: [lang_local_index], + <xsl:text> new_hmi_value: function(index, value, oldval) { + <xsl:text> let current_lang = switch_langnum(value); + <xsl:text> let [langname,langcode] = langs[current_lang]; + <xsl:text> apply_hmi_value(langcode_local_index, langcode); + <xsl:text> apply_hmi_value(langname_local_index, langname); + <xsl:text> switch_page(); + <xsl:text>// returns en_US, fr_FR or en_UK depending on selected language + <xsl:text>function get_current_lang_code(){ + <xsl:text> return cache[langcode_local_index]; + <xsl:text>function setup_lang(){ + <xsl:text> let current_lang = cache[lang_local_index]; + <xsl:text> let new_lang = switch_langnum(current_lang); + <xsl:text> if(current_lang != new_lang){ + <xsl:text> apply_hmi_value(lang_local_index, new_lang); + <xsl:text>setup_lang(); + <xsl:text>function update_subscriptions() { + <xsl:text> let delta = []; + <xsl:text> subscriptions_update_requested = false; + <xsl:text> if(!ws || ws.readyState != WebSocket.OPEN) + <xsl:text> // dont' change subscriptions if not connected + <xsl:text> for(let index in subscriptions){ + <xsl:text> let widgets = subscribers(index); + <xsl:text> // periods are in ms + <xsl:text> let previous_period = get_subscription_period(index); + <xsl:text> // subscribing with a zero period is unsubscribing + <xsl:text> let new_period = 0; + <xsl:text> if(widgets.size > 0) { + <xsl:text> let maxfreq = 0; + <xsl:text> for(let widget of widgets){ + <xsl:text> let wf = widget.frequency; + <xsl:text> if(wf != undefined && maxfreq < wf) + <xsl:text> maxfreq = wf; + <xsl:text> if(maxfreq != 0) + <xsl:text> new_period = 1000/maxfreq; + <xsl:text> if(previous_period != new_period) { + <xsl:text> set_subscription_period(index, new_period); + <xsl:text> if(index <= last_remote_index){ + <xsl:text> new Uint8Array([2]), /* subscribe = 2 */ + <xsl:text> new Uint32Array([index]), + <xsl:text> new Uint16Array([new_period])); + <xsl:text> send_blob(delta); + <xsl:text>function request_subscriptions_update(){ + <xsl:text> if(!subscriptions_update_requested){ + <xsl:text> subscriptions_update_requested = true; + <xsl:text> Promise.resolve().then(update_subscriptions); + <xsl:text>function send_hmi_value(index, value) { + <xsl:text> if(index > last_remote_index){ + <xsl:text> dispatch_value(index, value); + <xsl:text> if(persistent_indexes.has(index)){ + <xsl:text> let varname = persistent_indexes.get(index); + <xsl:text> document.cookie = varname+"="+value+"; max-age=3153600000"; + <xsl:text> let iectype = hmitree_types[index]; + <xsl:text> let tobinary = typedarray_types[iectype]; + <xsl:text> new Uint8Array([0]), /* setval = 0 */ + <xsl:text> new Uint32Array([index]), + <xsl:text> tobinary(value)]); + <xsl:text> // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf + <xsl:text> // cache[index] = value; + <xsl:text>function apply_hmi_value(index, new_val) { + <xsl:text> // Similarly to previous comment, taking decision to update based + <xsl:text> // on cache content is bad and can lead to inconsistency + <xsl:text> /*let old_val = cache[index];*/ + <xsl:text> if(new_val != undefined /*&& old_val != new_val*/) + <xsl:text> send_hmi_value(index, new_val); + <xsl:text> return new_val; + <xsl:text>const quotes = {"'":null, '"':null}; + <xsl:text>function eval_operation_string(old_val, opstr) { + <xsl:text> let op = opstr[0]; + <xsl:text> let given_val; + <xsl:text> if(opstr.length < 2) + <xsl:text> return undefined; + <xsl:text> if(opstr[1] in quotes){ + <xsl:text> if(opstr.length < 3) + <xsl:text> return undefined; + <xsl:text> if(opstr[opstr.length-1] == opstr[1]){ + <xsl:text> given_val = opstr.slice(2,opstr.length-1); + <xsl:text> given_val = Number(opstr.slice(1)); + <xsl:text> let new_val; + <xsl:text> new_val = given_val; + <xsl:text> new_val = old_val + given_val; + <xsl:text> new_val = old_val - given_val; + <xsl:text> new_val = old_val * given_val; + <xsl:text> new_val = old_val / given_val; + <xsl:text> return new_val; + <xsl:text>var current_visible_page; + <xsl:text>var current_subscribed_page; + <xsl:text>var current_page_index; + <xsl:text>var page_node_local_index = hmi_local_index("page_node"); + <xsl:text>var page_switch_in_progress = false; + <xsl:text>function toggleFullscreen() { + <xsl:text> let elem = document.documentElement; + <xsl:text> if (!document.fullscreenElement) { + <xsl:text> elem.requestFullscreen().catch(err => { + <xsl:text> console.log("Error attempting to enable full-screen mode: "+err.message+" ("+err.name+")"); + <xsl:text> document.exitFullscreen(); + <xsl:text>// prevents context menu from appearing on right click and long touch + <xsl:text>document.body.addEventListener('contextmenu', e => { + <xsl:text> toggleFullscreen(); + <xsl:text> e.preventDefault(); + <xsl:text>if(screensaver_delay){ + <xsl:text> var screensaver_timer = null; + <xsl:text> function reset_screensaver_timer() { + <xsl:text> if(screensaver_timer){ + <xsl:text> window.clearTimeout(screensaver_timer); + <xsl:text> screensaver_timer = window.setTimeout(() => { + <xsl:text> switch_page("ScreenSaver"); + <xsl:text> screensaver_timer = null; + <xsl:text> }, screensaver_delay*1000); + <xsl:text> document.body.addEventListener('pointerdown', reset_screensaver_timer); + <xsl:text> // initialize screensaver + <xsl:text> reset_screensaver_timer(); + <xsl:text>function detach_detachables() { + <xsl:text> for(let eltid in detachable_elements){ + <xsl:text> let [element,parent] = detachable_elements[eltid]; + <xsl:text> parent.removeChild(element); + <xsl:text>function switch_page(page_name, page_index) { + <xsl:text> if(page_switch_in_progress){ + <xsl:text> /* page switch already going */ + <xsl:text> /* TODO LOG ERROR */ + <xsl:text> return false; + <xsl:text> page_switch_in_progress = true; + <xsl:text> if(page_name == undefined) + <xsl:text> page_name = current_subscribed_page; + <xsl:text> else if(page_index == undefined){ + <xsl:text> [page_name, page_index] = page_name.split('@') + <xsl:text> let old_desc = page_desc[current_subscribed_page]; + <xsl:text> let new_desc = page_desc[page_name]; + <xsl:text> if(new_desc == undefined){ + <xsl:text> /* TODO LOG ERROR */ + <xsl:text> return false; + <xsl:text> if(page_index == undefined) + <xsl:text> page_index = new_desc.page_index; + <xsl:text> else if(typeof(page_index) == "string") { + <xsl:text> let hmitree_node = hmitree_nodes[page_index]; + <xsl:text> if(hmitree_node !== undefined){ + <xsl:text> let [int_index, hmiclass] = hmitree_node; + <xsl:text> if(hmiclass == new_desc.page_class) + <xsl:text> page_index = int_index; + <xsl:text> page_index = new_desc.page_index; + <xsl:text> page_index = new_desc.page_index; + <xsl:text> if(old_desc){ + <xsl:text> old_desc.widgets.map(([widget,relativeness])=>widget.unsub()); + <xsl:text> const new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index; + <xsl:text> const container_id = page_name + (page_index != undefined ? page_index : ""); + <xsl:text> new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness,container_id)); + <xsl:text> request_subscriptions_update(); + <xsl:text> current_subscribed_page = page_name; + <xsl:text> current_page_index = page_index; + <xsl:text> let page_node; + <xsl:text> if(page_index != undefined){ + <xsl:text> page_node = hmitree_paths[page_index]; + <xsl:text> page_node = ""; + <xsl:text> apply_hmi_value(page_node_local_index, page_node); + <xsl:text> jumps_need_update = true; + <xsl:text> requestHMIAnimation(); + <xsl:text> let [last_page_name, last_page_index] = jump_history[jump_history.length-1]; + <xsl:text> if(last_page_name != page_name || last_page_index != page_index){ + <xsl:text> jump_history.push([page_name, page_index]); + <xsl:text> if(jump_history.length > 42) + <xsl:text> jump_history.shift(); + <xsl:text> apply_hmi_value(current_page_var_index, page_index == undefined + <xsl:text> : page_name + "@" + hmitree_paths[page_index]); + <xsl:text> // when entering a page, assignments are evaluated + <xsl:text> new_desc.widgets[0][0].assign(); + <xsl:text> return true; + <xsl:text>function switch_visible_page(page_name) { + <xsl:text> let old_desc = page_desc[current_visible_page]; + <xsl:text> let new_desc = page_desc[page_name]; + <xsl:text> if(old_desc){ + <xsl:text> for(let eltid in old_desc.required_detachables){ + <xsl:text> if(!(eltid in new_desc.required_detachables)){ + <xsl:text> let [element, parent] = old_desc.required_detachables[eltid]; + <xsl:text> parent.removeChild(element); + <xsl:text> for(let eltid in new_desc.required_detachables){ + <xsl:text> if(!(eltid in old_desc.required_detachables)){ + <xsl:text> let [element, parent] = new_desc.required_detachables[eltid]; + <xsl:text> parent.appendChild(element); + <xsl:text> for(let eltid in new_desc.required_detachables){ + <xsl:text> let [element, parent] = new_desc.required_detachables[eltid]; + <xsl:text> parent.appendChild(element); + <xsl:text> svg_root.setAttribute('viewBox',new_desc.bbox.join(" ")); + <xsl:text> current_visible_page = page_name; + <xsl:text>/* From https://jsfiddle.net/ibowankenobi/1mmh7rs6/6/ */ + <xsl:text>function getAbsoluteCTM(element){ + <xsl:text> var height = svg_root.height.baseVal.value, + <xsl:text> width = svg_root.width.baseVal.value, + <xsl:text> viewBoxRect = svg_root.viewBox.baseVal, + <xsl:text> vHeight = viewBoxRect.height, + <xsl:text> vWidth = viewBoxRect.width; + <xsl:text> if(!vWidth || !vHeight){ + <xsl:text> return element.getCTM(); + <xsl:text> var sH = height/vHeight, + <xsl:text> sW = width/vWidth, + <xsl:text> matrix = svg_root.createSVGMatrix(); + <xsl:text> matrix.a = sW; + <xsl:text> matrix.d = sH + <xsl:text> var realCTM = element.getCTM().multiply(matrix.inverse()); + <xsl:text> realCTM.e = realCTM.e/sW + viewBoxRect.x; + <xsl:text> realCTM.f = realCTM.f/sH + viewBoxRect.y; + <xsl:text> return realCTM; + <xsl:text>function apply_reference_frames(){ + <xsl:text> const matches = svg_root.querySelectorAll("g[svghmi_x_offset]"); + <xsl:text> matches.forEach((group) => { + <xsl:text> let [x,y] = ["x", "y"].map((axis) => Number(group.getAttribute("svghmi_"+axis+"_offset"))); + <xsl:text> let ctm = getAbsoluteCTM(group); + <xsl:text> // zero translation part of CTM + <xsl:text> // to only apply rotation/skewing to offset vector + <xsl:text> let invctm = ctm.inverse(); + <xsl:text> let vect = new DOMPoint(x, y); + <xsl:text> let newvect = vect.matrixTransform(invctm); + <xsl:text> let transform = svg_root.createSVGTransform(); + <xsl:text> transform.setTranslate(newvect.x, newvect.y); + <xsl:text> group.transform.baseVal.appendItem(transform); + <xsl:text> ["x", "y"].forEach((axis) => group.removeAttribute("svghmi_"+axis+"_offset")); + <xsl:text>// prepare SVG + <xsl:text>apply_reference_frames(); + <xsl:text>init_widgets(); + <xsl:text>detach_detachables(); + <xsl:text>// show main page + <xsl:text>switch_page(default_page); + <xsl:text>var reconnect_delay = 0; + <xsl:text>var periodic_reconnect_timer; + <xsl:text>var force_reconnect = false; + <xsl:text>// Once connection established + <xsl:text>function ws_onopen(evt) { + <xsl:text> // Work around memory leak with websocket on QtWebEngine + <xsl:text> // reconnect every hour to force deallocate websocket garbage + <xsl:text> if(window.navigator.userAgent.includes("QtWebEngine")){ + <xsl:text> if(periodic_reconnect_timer){ + <xsl:text> window.clearTimeout(periodic_reconnect_timer); + <xsl:text> periodic_reconnect_timer = window.setTimeout(() => { + <xsl:text> force_reconnect = true; + <xsl:text> periodic_reconnect_timer = null; + <xsl:text> }, 3600000); + <xsl:text> // forget earlier subscriptions locally + <xsl:text> reset_subscription_periods(); + <xsl:text> // update PLC about subscriptions and current page + <xsl:text> switch_page(); + <xsl:text> // at first try reconnect immediately + <xsl:text> reconnect_delay = 1; + <xsl:text>function ws_onclose(evt) { + <xsl:text> console.log("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+" Reload in "+reconnect_delay+"ms."); + <xsl:text> // Do not attempt to reconnect immediately in case: + <xsl:text> // - connection was closed by server (PLC stop) + <xsl:text> // - connection was closed locally with an intention to reconnect + <xsl:text> if(evt.code=1000 && !force_reconnect){ + <xsl:text> window.alert("Connection closed by server"); + <xsl:text> location.reload(); + <xsl:text> window.setTimeout(create_ws, reconnect_delay); + <xsl:text> reconnect_delay += 500; + <xsl:text> force_reconnect = false; + <xsl:text> window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws') + <xsl:text> + '?mode=' + (has_watchdog ? "watchdog" : "multiclient"); + <xsl:text>function create_ws(){ + <xsl:text> ws = new WebSocket(ws_url); + <xsl:text> ws.binaryType = 'arraybuffer'; + <xsl:text> ws.onmessage = ws_onmessage; + <xsl:text> ws.onclose = ws_onclose; + <xsl:text> ws.onopen = ws_onopen; + <xsl:text>var edit_callback; + <xsl:text>const localtypes = {"PAGE_LOCAL":null, "HMI_LOCAL":null} + <xsl:text>function edit_value(path, valuetype, callback, initial) { + <xsl:text> if(valuetype in localtypes){ + <xsl:text> valuetype = (typeof initial) == "number" ? "HMI_REAL" : "HMI_STRING"; + <xsl:text> let [keypadid, xcoord, ycoord] = keypads[valuetype]; + <xsl:text> edit_callback = callback; + <xsl:text> let widget = hmi_widgets[keypadid]; + <xsl:text> widget.start_edit(path, valuetype, callback, initial); + <xsl:text>var current_modal; /* TODO stack ?*/ + <xsl:text>function show_modal() { + <xsl:text> let [element, parent] = detachable_elements[this.element.id]; + <xsl:text> tmpgrp = document.createElementNS(xmlns,"g"); + <xsl:text> tmpgrpattr = document.createAttribute("transform"); + <xsl:text> let [xcoord,ycoord] = this.coordinates; + <xsl:text> let [xdest,ydest] = page_desc[current_visible_page].bbox; + <xsl:text> tmpgrpattr.value = "translate("+String(xdest-xcoord)+","+String(ydest-ycoord)+")"; + <xsl:text> tmpgrp.setAttributeNode(tmpgrpattr); + <xsl:text> tmpgrp.appendChild(element); + <xsl:text> parent.appendChild(tmpgrp); + <xsl:text> current_modal = [this.element.id, tmpgrp]; + <xsl:text>function end_modal() { + <xsl:text> let [eltid, tmpgrp] = current_modal; + <xsl:text> let [element, parent] = detachable_elements[this.element.id]; + <xsl:text> parent.removeChild(tmpgrp); + <xsl:text> current_modal = undefined; +// Declarations from SVG scripts (inkscape document properties) + <xsl:for-each select="/svg:svg/svg:script"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="@id"/> + <xsl:value-of select="text()"/>