# -*- 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) -> dict[str, str]: 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) -> dict[str, str]: 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(mapping, str): values[key] = mapping continue mapped = mapping(self.__attribs(node, keys)) values[key] = mapped or default except: pass 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')