jw-python/tools/python/jwutils/graph/yed/MapAttr2Shape.py

191 lines
7.5 KiB
Python
Raw Normal View History

# -*- 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):
# <graphml><key for="node" id="d22" yfiles.type="nodegraphics"/></graphml>
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')