SVGHMI: detachable and discardable elements sets, Reworked geometric intersection, toward more accurate page content detection.
Moved page's widget/element dependency crawling functions so that it is possible to compute a global detachable and discardable elements sets.
Reworked geometric intersection detection logic to distinguish ovelapping and inclusion.
Goal is to include englobing and overlapping graphical elements, but not groups (would then include everything around...). Intermediate commit, to be continued.
include yslt_noindent.yml2
// overrides yslt's output function to set CDATA
decl output(method, cdata-section-elements="xhtml:script");
in xsl decl labels(*ptr, name="defs_by_labels") alias call-template {
with "hmi_element", "$hmi_element";
with "labels"{text *ptr};
};
in xsl decl optional_labels(*ptr, name="defs_by_labels") alias call-template {
with "hmi_element", "$hmi_element";
with "labels"{text *ptr};
with "mandatory","'no'";
};
in xsl decl svgtmpl(match, xmlns="http://www.w3.org/2000/svg") alias template;
istylesheet
/* From Inkscape */
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
/* Our namespace to invoke python code */
xmlns:ns="beremiz"
extension-element-prefixes="ns func"
exclude-result-prefixes="ns str regexp exsl func" {
/* This retrieves geometry obtained through "inkscape -S"
* already parsed by python and presented as a list of
* <bbox x="0" y="0" w="42" h="42">
*/
const "geometry", "ns:GetSVGGeometry()";
const "hmitree", "ns:GetHMITree()";
const "svg_root_id", "/svg:svg/@id";
const "hmi_elements", "//svg:*[starts-with(@inkscape:label, 'HMI:')]";
const "hmi_geometry", "$geometry[@Id = $hmi_elements/@id]";
const "hmi_pages", "$hmi_elements[func:parselabel(@inkscape:label)/widget/@type = 'Page']";
const "default_page" choose {
when "count($hmi_pages) > 1" {
const "Home_page",
"$hmi_pages[func:parselabel(@inkscape:label)/widget/arg[1]/@value = 'Home']";
choose {
when "$Home_page" > Home
otherwise {
error "No Home page defined!";
}
}
}
when "count($hmi_pages) = 0" {
error "No page defined!";
}
otherwise > «func:parselabel($hmi_pages/@inkscape:label)/widget/arg[1]/@value»
}
const "_categories" {
noindex > HMI_ROOT
noindex > HMI_NODE
noindex > HMI_PLC_STATUS
noindex > HMI_CURRENT_PAGE
}
const "categories", "exsl:node-set($_categories)";
const "_indexed_hmitree" apply "$hmitree", mode="index";
const "indexed_hmitree", "exsl:node-set($_indexed_hmitree)";
// returns all directly or indirectly refered elements
def "func:refered_elements" {
param "elems";
const "descend", "$elems/descendant-or-self::svg:*";
const "clones", "$descend[self::svg:use]";
const "originals", "//svg:*[concat('#',@id) = $clones/@xlink:href]";
choose {
when "$originals"
result "$descend | func:refered_elements($originals)";
otherwise
result "$descend";
}
}
def "func:intersect_1d" {
/* it is assumed that a1 > a0 and b1 > b0 */
param "a0";
param "a1";
param "b0";
param "b1";
const "d0", "$a0 >= $b0";
const "d1", "$a1 >= $b1";
choose {
when "not($d0) and $d1"
result "3"; /* a included in b */
when "$d0 and not($d1)"
result "2"; /* b included in a */
when "$d0 = $d1 and $b0 < $a1"
result "1"; /* a and b are overlapped */
otherwise
result "0"; /* no intersection*/
}
}
def "func:intersect" {
param "a";
param "b";
const "x_intersect", "func:intersect_1d($a/@x, $a/@x+$a/@w, $b/@x, $b/@x+$b/@w)";
choose{
when "$x_intersect != 0"{
const "y_intersect", "func:intersect_1d($a/@y, $a/@y+$a/@w, $b/@y, $b/@y+$b/@w)";
result "$x_intersect * $y_intersect";
}
otherwise result "0";
}
}
// return overlapping geometry a given element
def "func:overlapping_geometry" {
param "elt";
/* only included groups are returned */
/* all other elemenst are returne when overlapping*/
const "g", "$geometry[@Id = $elt/@id]";
result """$geometry[@Id != $elt/@id and func:intersect(., $g) = 4]""";
}
def "func:sumarized_elements" {
param "elements";
const "short_list", "$elements[not(ancestor::*/@id = $elements/@id)]";
/* TODO exclude globally discardable elements from group fulfillment check */
const "filled_groups", "$short_list/parent::svg:*[not(descendant::*[not(self::svg:g)][not(@id = $short_list/descendant-or-self::*[not(self::svg:g)]/@id)])]";
const "groups_to_add", "$filled_groups[not(ancestor::*/@id = $filled_groups/@id)]";
result "$groups_to_add | $short_list[not(ancestor::svg:g/@id = $filled_groups/@id)]";
}
def "func:all_related_elements" {
param "page";
const "page_overlapping_geometry", "func:overlapping_geometry($page)";
const "page_overlapping_elements", "//svg:*[@id = $page_overlapping_geometry/@Id]";
const "page_sub_elements", "func:refered_elements($page | $page_overlapping_elements)";
result "$page_sub_elements";
}
def "func:detachable_elements" {
param "pages";
choose{
when "$pages"{
result """func:sumarized_elements(func:all_related_elements($pages[1]))
| func:detachable_elements($pages[position()!=1])""";
}otherwise{
result "/..";
}
}
}
const "detachable_elements", "func:detachable_elements($hmi_pages)";
const "essential_elements", "$detachable_elements | /svg:svg/svg:defs";
const "required_elements", "$essential_elements//svg:* | $essential_elements/ancestor-or-self::svg:*";
const "discardable_elements", "//svg:*[not(@id = $required_elements/@id)]";
template "*", mode="index" {
param "index", "0";
param "parentpath", "''";
const "content" {
const "path"
choose {
when "local-name() = 'HMI_ROOT'" > «$parentpath»
otherwise > «$parentpath»/«@name»
}
choose {
when "not(local-name() = $categories/noindex)" {
xsl:copy {
attrib "index" > «$index»
attrib "hmipath" > «$path»
foreach "@*" xsl:copy;
}
/* no node expected below value nodes */
}
otherwise {
apply "*[1]", mode="index"{
with "index", "$index";
with "parentpath" > «$path»
}
}
}
}
copy "$content";
apply "following-sibling::*[1]", mode="index" {
with "index", "$index + count(exsl:node-set($content)/*)";
with "parentpath" > «$parentpath»
}
}
/* Identity template :
* - copy every attributes
* - copy every sub-elements
*/
template "@* | node()", mode="inline_svg" {
/* use real xsl:copy instead copy-of alias from yslt.yml2 */
xsl:copy apply "@* | node()", mode="inline_svg";
/* TODO filter out globally discardable elements */
}
/* replaces inkscape's height and width hints. forces fit */
template "svg:svg/@width", mode="inline_svg";
template "svg:svg/@height", mode="inline_svg";
svgtmpl "svg:svg", mode="inline_svg" svg {
attrib "preserveAspectRatio" > none
attrib "height" > 100vh
attrib "width" > 100vw
apply "@* | node()", mode="inline_svg";
}
/* ensure that coordinate in CSV file generated by inkscape are in default reference frame */
template "svg:svg[@viewBox!=concat('0 0 ', @width, ' ', @height)]", mode="inline_svg" {
error > ViewBox settings other than X=0, Y=0 and Scale=1 are not supported
}
/* ensure that coordinate in CSV file generated by inkscape match svg default unit */
template "sodipodi:namedview[@units!='px' or @inkscape:document-units!='px']", mode="inline_svg" {
error > All units must be set to "px" in Inkscape's document properties
}
//// Commented out before implementing runtime DOM remove/append on page switch - would have side effect
////
//// /* clone unlinkink until widget for better perf with webkit */
//// svgtmpl "svg:use", mode="inline_svg"
//// {
//// g{
//// attrib "style" > «@style»
//// attrib "transform" > «@transform»
//// /* keep same id and label in case it is a widget */
//// //attrib "inkscape:label","@inkscape:label";
//// attrib "id" > «@id»
//// const "targetid","substring-after(@xlink:href,'#')";
//// apply "//svg:*[@id = $targetid]", mode="unlink_clone";
//// }
//// }
//// svgtmpl "@*", mode="unlink_clone" xsl:copy;
//// svgtmpl "svg:*", mode="unlink_clone" {
//// choose {
//// when "@id = $hmi_elements/@id" {
//// use{
//// attrib "xlink:href" > «concat('#',@id)»
//// }
//// }
//// otherwise {
//// xsl:copy apply "@* | node()", mode="unlink_clone";
//// }
//// }
//// }
// template "svg:use/@style", mode="inline_svg"{
// attrib "style" > all:initial;
// //«.»
// }
// template "svg:*[concat('#',@id) = //svg:use/@xlink:href]/@style", mode="inline_svg"{
// attrib "style" > all:unset;
// //«.»
// }
/*const "mark" > =HMI=\n*/
/* copy root node and add geometry as comment for a test */
template "/" {
comment > Made with SVGHMI. https://beremiz.org
/* DEBUG DATA */
comment {
apply "$hmi_geometry", mode="testgeo";
}
comment {
apply "$hmitree", mode="testtree";
}
comment {
apply "$indexed_hmitree", mode="testtree";
}
comment {
| Detachable :
foreach "$detachable_elements"{
| «@id»
}
}
comment {
| Discardable :
foreach "$discardable_elements"{
| «@id»
}
}
/**/
html xmlns="http://www.w3.org/1999/xhtml"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" {
head;
body style="margin:0;overflow:hidden;" {
apply "svg:svg", mode="inline_svg";
script{
call "scripts";
}
}
}
}
/*
Parses:
"HMI:WidgetType:param1:param2@path1@path2"
Into:
widget type="WidgetType" {
arg value="param1";
arg value="param2";
path value="path1";
path value="path2";
}
*/
def "func:parselabel" {
param "label";
const "description", "substring-after($label,'HMI:')";
const "_args", "substring-before($description,'@')";
const "args" choose {
when "$_args" value "$_args";
otherwise value "$description";
}
const "_type", "substring-before($args,':')";
const "type" choose {
when "$_type" value "$_type";
otherwise value "$args";
}
const "ast" if "$type" widget {
attrib "type" > «$type»
foreach "str:split(substring-after($args, ':'), ':')" {
arg {
attrib "value" > «.»
}
}
const "paths", "substring-after($description,'@')";
foreach "str:split($paths, '@')" {
path {
attrib "value" > «.»
}
}
}
result "exsl:node-set($ast)";
}
function "scripts"
{
| //(function(){
|
| var hmi_hash = [«$hmitree/@hash»];
/* TODO re-enable
||
function evaluate_js_from_descriptions() {
var Page;
var Input;
var Display;
var res = [];
||
const "midmark" > \n«$mark»
apply """//*[contains(child::svg:desc, $midmark) or \
starts-with(child::svg:desc, $mark)]""",2
mode="code_from_descs";
||
return res;
}
||
*/
| var hmi_widgets = {
foreach "$hmi_elements" {
const "widget", "func:parselabel(@inkscape:label)/widget";
| "«@id»": {
| type: "«$widget/@type»",
| args: [
foreach "$widget/arg"
| "«@value»"`if "position()!=last()" > ,`
| ],
| indexes: [
foreach "$widget/path" {
const "hmipath","@value";
const "hmitree_match","$indexed_hmitree/*[@hmipath = $hmipath]";
choose {
when "count($hmitree_match) = 0" {
warning > No match for path "«$hmipath»" in HMI tree
}
otherwise {
| «$hmitree_match/@index»`if "position()!=last()" > ,`
}
}
}
| ],
| element: document.getElementById("«@id»"),
apply "$widget", mode="widget_defs" with "hmi_element",".";
| }`if "position()!=last()" > ,`
}
| }
|
| var heartbeat_index = «$indexed_hmitree/*[@hmipath = '/HEARTBEAT']/@index»;
|
| var hmitree_types = [
foreach "$indexed_hmitree/*" {
| /* «@index» «@hmipath» */ "«substring(local-name(), 5)»"`if "position()!=last()" > ,`
}
| ]
|
| var page_desc = {
foreach "$hmi_pages" {
const "desc", "func:parselabel(@inkscape:label)/widget";
const "page", ".";
const "p", "$geometry[@Id = $page/@id]";
const "page_all_elements", "func:all_related_elements($page)";
const "all_page_ids","$page_all_elements[@id = $hmi_elements/@id and @id != $page/@id]/@id";
const "shorter_list", "func:sumarized_elements($page_all_elements)";
| "«$desc/arg[1]/@value»": {
| widget: hmi_widgets["«@id»"],
| bbox: [«$p/@x», «$p/@y», «$p/@w», «$p/@h»],
| widgets: [
foreach "$all_page_ids" {
| hmi_widgets["«.»"]`if "position()!=last()" > ,`
}
| ],
| required_elements: [
foreach "$shorter_list" {
| "«@id»",
}
| ]
| }`if "position()!=last()" > ,`
}
| }
|
| var default_page = "«$default_page»";
| var svg_root = document.getElementById("«$svg_root_id»");
include text svghmi.js
| //})();
}
// template "*", mode="code_from_descs" {
// ||
// {
// var path, role, name, priv;
// var id = "«@id»";
// ||
// /* if label is used, use it as default name */
// if "@inkscape:label"
// |> name = "«@inkscape:label»";
// | /* -------------- */
// // this breaks indent, but fixing indent could break string literals
// value "substring-after(svg:desc, $mark)";
// // nobody reads generated code anyhow...
// ||
// /* -------------- */
// res.push({
// path:path,
// role:role,
// name:name,
// priv:priv
// })
// }
// ||
// }
/**/
template "bbox", mode="testgeo"{
| ID: «@Id» x: «@x» y: «@y» w: «@w» h: «@h»
}
template "*", mode="testtree"{
param "indent", "''";
> «$indent» «local-name()»
foreach "@*" > «local-name()»=«.»
> \n
apply "*", mode="testtree" {
with "indent" value "concat($indent,'>')"
};
}
/**/
function "defs_by_labels" {
param "labels","''";
param "mandatory","'yes'";
param "hmi_element";
const "widget_type","@type";
foreach "str:split($labels)" {
const "name",".";
const "elt_id","$hmi_element//*[@inkscape:label=$name][1]/@id";
choose {
when "not($elt_id)" {
if "$mandatory='yes'" {
// TODO FIXME error > «$widget_type» widget must have a «$name» element
warning > «$widget_type» widget must have a «$name» element
}
// otherwise produce nothing
}
otherwise {
| «$name»_elt: document.getElementById("«$elt_id»"),
}
}
}
}
template "widget[@type='Display']", mode="widget_defs" {
param "hmi_element";
| frequency: 5,
| dispatch: function(value) {
choose {
when "$hmi_element[self::svg:text]"{
// TODO : care about <tspan> ?
| this.element.textContent = String(value);
}
otherwise {
warning > Display widget as a group not implemented
}
}
| },
}
template "widget[@type='Meter']", mode="widget_defs" {
param "hmi_element";
| frequency: 10,
labels("value min max needle range");
| dispatch: function(value) {
| this.value_elt.textContent = String(value);
| let [min,max,totallength] = this.range;
| let length = Math.max(0,Math.min(totallength,(Number(value)-min)*totallength/(max-min)));
| let tip = this.range_elt.getPointAtLength(length);
// TODO : deal with transformations between needle and range
| this.needle_elt.setAttribute('d', "M "+this.origin.x+","+this.origin.y+" "+tip.x+","+tip.y);
| },
| origin: undefined,
| range: undefined,
| init: function() {
| this.range = [Number(this.min_elt.textContent), Number(this.max_elt.textContent), this.range_elt.getTotalLength()]
| this.origin = this.needle_elt.getPointAtLength(0);
| },
}
def "func:escape_quotes" {
param "txt";
// have to use a python string to enter escaped quote
const "frst", !"substring-before($txt,'\"')"!;
const "frstln", "string-length($frst)";
choose {
when "$frstln > 0 and string-length($txt) > $frstln" {
result !"concat($frst,'\\\"',func:escape_quotes(substring-after($txt,'\"')))"!;
}
otherwise {
result "$txt";
}
}
}
template "widget[@type='Input']", mode="widget_defs" {
param "hmi_element";
const "value_elt" {
optional_labels("value");
}
value "$value_elt";
if "$value_elt"
| frequency: 5,
| dispatch: function(value) {
if "$value_elt"
| this.value_elt.textContent = String(value);
| },
const "edit_elt_id","$hmi_element/*[@inkscape:label='edit'][1]/@id";
| init: function() {
if "$edit_elt_id" {
| document.getElementById("«$edit_elt_id»").addEventListener(
| "click",
| evt => alert('XXX TODO : Edit value'));
}
foreach "$hmi_element/*[regexp:test(@inkscape:label,'^[=+\-].+')]" {
| document.getElementById("«@id»").addEventListener(
| "click",
| evt => {let new_val = change_hmi_value(this.indexes[0], "«func:escape_quotes(@inkscape:label)»");
| this.value_elt.textContent = String(new_val);});
/* could gray out value until refreshed */
}
| },
}
template "widget[@type='Button']", mode="widget_defs" {
}
template "widget[@type='Toggle']", mode="widget_defs" {
| frequency: 5,
}
template "widget[@type='Switch']", mode="widget_defs" {
param "hmi_element";
| frequency: 5,
| dispatch: function(value) {
| for(let choice of this.choices){
| if(value != choice.value){
| choice.elt.setAttribute("style", "display:none");
| } else {
| choice.elt.setAttribute("style", choice.style);
| }
| }
| },
| init: function() {
| // Hello Switch
| },
| choices: [
const "regex",!"'^(\"[^\"].*\"|\-?[0-9]+)(#.*)?$'"!;
foreach "$hmi_element/*[regexp:test(@inkscape:label,$regex)]" {
const "literal", "regexp:match(@inkscape:label,$regex)[2]";
| {
| elt:document.getElementById("«@id»"),
| style:"«@style»",
| value:«$literal»
| }`if "position()!=last()" > ,`
}
| ],
}
template "widget[@type='Jump']", mode="widget_defs" {
param "hmi_element";
| on_click: function(evt) {
| console.log(evt);
| switch_page(this.args[0]);
| },
| init: function() {
/* registering event this way doies not "click" through svg:use
| this.element.onclick = evt => switch_page(this.args[0]);
event must be registered by adding attribute to element instead
TODO : generalize mouse event handling by global event capture + getElementsAtPoint()
*/
| this.element.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_click(evt)");
| },
}
}