svghmi/hmi_tree.py
changeset 3302 c89fc366bebd
parent 3224 507dd7bc8cb5
child 3381 3a0908b0319d
equal deleted inserted replaced
2744:577118ebd179 3302:c89fc366bebd
       
     1 #!/usr/bin/env python
       
     2 # -*- coding: utf-8 -*-
       
     3 
       
     4 # This file is part of Beremiz
       
     5 # Copyright (C) 2021: Edouard TISSERANT
       
     6 #
       
     7 # See COPYING file for copyrights details.
       
     8 
       
     9 from __future__ import absolute_import
       
    10 from itertools import izip, imap
       
    11 from pprint import pformat
       
    12 import weakref
       
    13 import hashlib
       
    14 
       
    15 from lxml import etree
       
    16 
       
    17 HMI_TYPES_DESC = {
       
    18     "HMI_NODE":{},
       
    19     "HMI_STRING":{},
       
    20     "HMI_INT":{},
       
    21     "HMI_BOOL":{},
       
    22     "HMI_REAL":{}
       
    23 }
       
    24 
       
    25 HMI_TYPES = HMI_TYPES_DESC.keys()
       
    26 
       
    27 class HMITreeNode(object):
       
    28     def __init__(self, path, name, nodetype, iectype = None, vartype = None, cpath = None, hmiclass = None):
       
    29         self.path = path
       
    30         self.name = name
       
    31         self.nodetype = nodetype
       
    32         self.hmiclass = hmiclass
       
    33         self.parent = None
       
    34 
       
    35         if iectype is not None:
       
    36             self.iectype = iectype
       
    37             self.vartype = vartype
       
    38             self.cpath = cpath
       
    39 
       
    40         if nodetype in ["HMI_NODE"]:
       
    41             self.children = []
       
    42 
       
    43     def pprint(self, indent = 0):
       
    44         res = ">"*indent + pformat(self.__dict__, indent = indent, depth = 1) + "\n"
       
    45         if hasattr(self, "children"):
       
    46             res += "\n".join([child.pprint(indent = indent + 1)
       
    47                               for child in self.children])
       
    48             res += "\n"
       
    49 
       
    50         return res
       
    51 
       
    52     def place_node(self, node):
       
    53         best_child = None
       
    54         known_best_match = 0
       
    55         potential_siblings = {}
       
    56         for child in self.children:
       
    57             if child.path is not None:
       
    58                 in_common = 0
       
    59                 for child_path_item, node_path_item in izip(child.path, node.path):
       
    60                     if child_path_item == node_path_item:
       
    61                         in_common +=1
       
    62                     else:
       
    63                         break
       
    64                 # Match can only be HMI_NODE, and the whole path of node
       
    65                 # must match candidate node (except for name part)
       
    66                 # since candidate would become child of that node
       
    67                 if in_common > known_best_match and \
       
    68                    child.nodetype == "HMI_NODE" and \
       
    69                    in_common == len(child.path) - 1:
       
    70                     known_best_match = in_common
       
    71                     best_child = child
       
    72                 else:
       
    73                     potential_siblings[child.path[
       
    74                         -2 if child.nodetype == "HMI_NODE" else -1]] = child
       
    75         if best_child is not None:
       
    76             if node.nodetype == "HMI_NODE" and best_child.path[:-1] == node.path[:-1]:
       
    77                 return "Duplicate_HMI_NODE", best_child
       
    78             return best_child.place_node(node)
       
    79         else:
       
    80             candidate_name = node.path[-2 if node.nodetype == "HMI_NODE" else -1]
       
    81             if candidate_name in potential_siblings:
       
    82                 return "Non_Unique", potential_siblings[candidate_name]
       
    83 
       
    84             if node.nodetype == "HMI_NODE" and len(self.children) > 0:
       
    85                 prev = self.children[-1]
       
    86                 if prev.path[:-1] == node.path[:-1]:
       
    87                     return "Late_HMI_NODE",prev
       
    88 
       
    89             node.parent = weakref.ref(self)
       
    90             self.children.append(node)
       
    91             return None
       
    92 
       
    93     def etree(self, add_hash=False):
       
    94 
       
    95         attribs = dict(name=self.name)
       
    96         if self.path is not None:
       
    97             attribs["path"] = ".".join(self.path)
       
    98 
       
    99         if self.hmiclass is not None:
       
   100             attribs["class"] = self.hmiclass
       
   101 
       
   102         if add_hash:
       
   103             attribs["hash"] = ",".join(map(str,self.hash()))
       
   104 
       
   105         res = etree.Element(self.nodetype, **attribs)
       
   106 
       
   107         if hasattr(self, "children"):
       
   108             for child_etree in imap(lambda c:c.etree(), self.children):
       
   109                 res.append(child_etree)
       
   110 
       
   111         return res
       
   112 
       
   113     @classmethod
       
   114     def from_etree(cls, enode):
       
   115         """
       
   116         alternative constructor, restoring HMI Tree from XML backup
       
   117         note: all C-related information is gone, 
       
   118               this restore is only for tree display and widget picking
       
   119         """
       
   120         nodetype = enode.tag
       
   121         attributes = enode.attrib
       
   122         name = attributes["name"]
       
   123         path = attributes["path"].split('.') if "path" in attributes else None 
       
   124         hmiclass = attributes.get("class", None)
       
   125         # hash is computed on demand
       
   126         node = cls(path, name, nodetype, hmiclass=hmiclass)
       
   127         for child in enode.iterchildren():
       
   128             newnode = cls.from_etree(child)
       
   129             newnode.parent = weakref.ref(node)
       
   130             node.children.append(newnode)
       
   131         return node
       
   132 
       
   133     def traverse(self):
       
   134         yield self
       
   135         if hasattr(self, "children"):
       
   136             for c in self.children:
       
   137                 for yoodl in c.traverse():
       
   138                     yield yoodl
       
   139 
       
   140     def hmi_path(self):
       
   141         if self.parent is None:
       
   142             return "/"
       
   143         p = self.parent()
       
   144         if p.parent is None:
       
   145             return "/" + self.name
       
   146         return p.hmi_path() + "/" + self.name
       
   147 
       
   148     def hash(self):
       
   149         """ Produce a hash, any change in HMI tree structure change that hash """
       
   150         s = hashlib.new('md5')
       
   151         self._hash(s)
       
   152         # limit size to HMI_HASH_SIZE as in svghmi.c
       
   153         return map(ord,s.digest())[:8]
       
   154 
       
   155     def _hash(self, s):
       
   156         s.update(str((self.name,self.nodetype)))
       
   157         if hasattr(self, "children"):
       
   158             for c in self.children:
       
   159                 c._hash(s)
       
   160 
       
   161 SPECIAL_NODES = [("HMI_ROOT", "HMI_NODE"),
       
   162                  ("heartbeat", "HMI_INT")]
       
   163                  # ("current_page", "HMI_STRING")])
       
   164