diff --git a/tools/python/jwutils/graph/Makefile b/tools/python/jwutils/graph/Makefile new file mode 100644 index 0000000..59b3ac1 --- /dev/null +++ b/tools/python/jwutils/graph/Makefile @@ -0,0 +1,4 @@ +TOPDIR = ../../../.. + +include $(TOPDIR)/make/proj.mk +include $(JWBDIR)/make/py-mod.mk diff --git a/tools/python/jwutils/graph/yed/Makefile b/tools/python/jwutils/graph/yed/Makefile new file mode 100644 index 0000000..781b0c8 --- /dev/null +++ b/tools/python/jwutils/graph/yed/Makefile @@ -0,0 +1,4 @@ +TOPDIR = ../../../../.. + +include $(TOPDIR)/make/proj.mk +include $(JWBDIR)/make/py-mod.mk diff --git a/tools/python/jwutils/graph/yed/MapAttr2Shape.py b/tools/python/jwutils/graph/yed/MapAttr2Shape.py new file mode 100644 index 0000000..1962322 --- /dev/null +++ b/tools/python/jwutils/graph/yed/MapAttr2Shape.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- + +from collections.abc import Callable + +import xml.etree.ElementTree as ET + +from jwutils.log import * + +class MapAttr2Shape: # export + + def __init__(self, mappings: dict[str, str|Callable[[dict[str, str]], str]]|None=None) -> None: + self.__mappings = mappings if mappings is not None else {} + self.__shape_node_key = 'd25' + self.__ns_gml = "http://graphml.graphdrawing.org/xmlns" + self.__ns = { + # -- Standard GraphML + "": self.__ns_gml, + "xsi": "http://www.w3.org/2001/XMLSchema-instance", + "xsi:schemaLocation": "http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd", + + # -- YWorks GraphML + "java": "http://www.yworks.com/xml/yfiles-common/1.0/java", + "sys": "http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0", + "x": "http://www.yworks.com/xml/yfiles-common/markup/2.0", + "y": "http://www.yworks.com/xml/graphml", + "yed": "http://www.yworks.com/xml/yed/3", + } + # https://stackoverflow.com/questions/4997848/ + for name, url in self.__ns.items(): + ET.register_namespace(name, url) + + def __keys(self, root): + ret: dict[str, str] = {} + for el in root.findall('key', self.__ns): + attr_name = el.get('attr.name') + id = el.get('id') + if attr_name is not None: + ret[attr_name] = id + return ret + + def __value(self, node, key): + data = node.find(f'data[@key="{key}"]', self.__ns) + if data is None: + return None + return data.text + + def __attribs(self, node, keys): + ret: dict[str, str] = {} + for name, key in keys.items(): + val = self.__value(node, key) + if val is None: + continue + ret[name] = val + return ret + + def __add_key_nodegraphics(self, root): + # + el = ET.Element('key') + for attr, val in { + 'id': self.__shape_node_key, + 'for': 'node', + 'yfiles.type':'nodegraphics' + }.items(): + el.set(attr, val) + root.append(el) + + def __massage_node(self, node, keys: dict[str, str]) -> None: + + def __add(parent, d: dict[str, Any]): + for tag, content in d.items(): + if tag.find('y:') != -1: + ns, tag = tag.split(':') + tag = '{' + self.__ns[ns] + '}' + tag + attrib = content.get('a') or {} + el = ET.Element(tag, attrib=attrib) + text = content.get('t') + if text is not None: + el.text = text + parent.append(el) + children = content.get('c') + if children is not None: + __add(el, children) + + default_values = { + 'color': '#FFCC00', + 'text': '' + } + values = {} + + for key, default in default_values.items(): + mapping = self.__mappings.get(key) + if mapping is None: + values[key] = default + continue + try: + if isinstance(str, mapping): + values[key] = mapping + continue + except: + pass + mapped = mapping(self.__attribs(node, keys)) + values[key] = mapped or default + + color = values['color'] + text = values['text'] + has_text = 'true' if text else 'false' + + width_text = round(len(text) * 5.75, 5) if text else 0 + width_box = width_text + 10 if text else 30 + + shape = { + 'data': { + 'a': {'key': self.__shape_node_key}, + 'c': { + 'y:ShapeNode': { + 'a': {}, + 'c': { + 'y:Geometry': {'a': {'height': '30.0', 'width': str(width_box), 'x': str(-(width_box / 2)), 'y':' -15.0'}}, + 'y:Fill': {'a': {'color': color, 'transparent': 'false'}}, + 'y:BorderStyle': {'a': {'color': '#000000', 'raised': 'false', 'type': 'line', 'width': '1.0'}}, + 'y:NodeLabel': { + 'a': { + 'alignment': 'center', + 'autoSizePolicy': 'content', + 'fontFamily': 'Dialog', + 'fontSize': '12', + 'fontStyle': 'plain', + 'hasBackgroundColor': 'false', + 'hasLineColor': 'false', + 'hasText': has_text, + 'height': '18', + 'horizontalTextPosition': 'center', + 'iconTextGap': '4', + 'modelName': 'custom', + 'textColor': '#000000', + 'verticalTextPosition': 'bottom', + 'visible': 'true', + 'width': str(width_text), + 'x': '13.0', + 'y': '13.0', + }, + 'c': { + 'y:LabelModel': { + 'c': { + 'y:SmartNodeLabelModel': {'a': {'distance': '4.0'}} + }, + }, + 'y:ModelParameter': { + 'c': { + 'y:SmartNodeLabelModelParameter': { + 'a': { + 'labelRatioX':'0.0', + 'labelRatioY': '0.0', + 'nodeRatioX': '0.0', + 'nodeRatioY': '0.0', + 'offsetX': '0.0', + 'offsetY': '0.0', + 'upX': '0.0', + 'upY': '-1.0', + } + } + } + } + }, + 't': text + }, + 'y:Shape': {'a': {'type': 'rectangle'}} + } + } + } + } + } + + __add(node, shape) + + def __massage_nodes(self, root) -> None: + keys = self.__keys(root) + graph = root.find(f'graph', self.__ns) + for node in graph: + self.__massage_node(node, keys) + + def run(self, path_in: str, path_out: str) -> None: + parser = ET.XMLParser(encoding="utf-8") + tree = ET.parse(path_in, parser=parser) + root = tree.getroot() + + self.__add_key_nodegraphics(root) + self.__massage_nodes(root) + + ET.indent(tree, space=' ', level=0) + tree.write(path_out, xml_declaration=True, encoding='utf-8')