mirror of
ssh://git.janware.com/srv/git/janware/proj/jw-python
synced 2026-01-15 09:53:32 +01:00
MapAttr2Shape is a class which allows processing GraphML files, such that nodes get added shape attributes understood by the yEd editor, based on the values of other node attributes. This allows to programmatically visualize node attributes. Signed-off-by: Jan Lindemann <jan@janware.com>
191 lines
7.5 KiB
Python
191 lines
7.5 KiB
Python
# -*- 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')
|