From 7f10e130641b38e98359485bb6df936e9f5d7264 Mon Sep 17 00:00:00 2001 From: huyenngn Date: Tue, 23 Jul 2024 22:58:19 +0200 Subject: [PATCH 01/12] feat: Add Element Relation View --- capellambse_context_diagrams/__init__.py | 36 +++++ .../collectors/element_relation_view.py | 131 ++++++++++++++++++ capellambse_context_diagrams/context.py | 55 +++++--- 3 files changed, 202 insertions(+), 20 deletions(-) create mode 100644 capellambse_context_diagrams/collectors/element_relation_view.py diff --git a/capellambse_context_diagrams/__init__.py b/capellambse_context_diagrams/__init__.py index 2db3b82f..026af306 100644 --- a/capellambse_context_diagrams/__init__.py +++ b/capellambse_context_diagrams/__init__.py @@ -66,6 +66,7 @@ def init() -> None: register_tree_view() register_realization_view() register_data_flow_view() + register_element_relation_view() # register_functional_context() XXX: Future @@ -272,3 +273,38 @@ def register_data_flow_view() -> None: for class_, dgcls, default_render_params in supported_classes: accessor = context.DataFlowAccessor(dgcls.value, default_render_params) common.set_accessor(class_, "data_flow_view", accessor) + + +def register_element_relation_view() -> None: + + supported_classes = [ + ( + oa.CommunicationMean, + { + DiagramType.OAB, + }, + ), + ( + fa.ComponentExchange, + { + DiagramType.SAB, + DiagramType.LAB, + DiagramType.PAB, + }, + ), + ] + + styles: dict[str, dict[str, capstyle.CSSdef]] = {} + for _class, _diagclasses in supported_classes: + common.set_accessor( + _class, + "element_relation_view", + context.ElementRelationAccessor("ElementRelationView Diagram"), + ) + for dgcls in _diagclasses: + styles.update(capstyle.STYLES.get(dgcls.value, {})) + + capstyle.STYLES["ElementRelationView Diagram"] = styles + capstyle.STYLES["ElementRelationView Diagram"].update( + capstyle.STYLES["Class Diagram Blank"] + ) diff --git a/capellambse_context_diagrams/collectors/element_relation_view.py b/capellambse_context_diagrams/collectors/element_relation_view.py new file mode 100644 index 00000000..edaab36a --- /dev/null +++ b/capellambse_context_diagrams/collectors/element_relation_view.py @@ -0,0 +1,131 @@ +# SPDX-FileCopyrightText: 2022 Copyright DB InfraGO AG and the capellambse-context-diagrams contributors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import logging +import math +import typing as t + +from capellambse.model import common +from capellambse.model.crosslayer import cs, fa, information +from capellambse.model.modeltypes import DiagramType as DT + +from .. import _elkjs, context +from . import generic, makers, tree_view + +logger = logging.getLogger(__name__) + +DEFAULT_LAYOUT_OPTIONS: _elkjs.LayoutOptions = { + "algorithm": "sporeOverlap", + "elk.underlyingLayoutAlgorithm": "radial", + "elk.spacing.nodeNode": 20, + "elk.spacing.edgeNode": 10, + "elk.edgeLabels.inline": True, + "elk.radial.compactor": "WEDGE_COMPACTION", +} + + +class ElementRelationProcessor: + def __init__( + self, + diagram: context.ElementRelationViewDiagram, + data: _elkjs.ELKInputData, + *, + params: dict[str, t.Any] | None = None, + ) -> None: + self.diagram = diagram + self.data = data + self.params = params or {} + self.global_boxes: dict[str, _elkjs.ELKInputChild] = {} + self.classes: dict[str, information.Class] = {} + + def process(self) -> None: + for item in self.diagram.target.allocated_exchange_items: + if not (parent_box := self.global_boxes.get(item.parent.uuid)): + parent_box = self.global_boxes.setdefault( + item.parent.uuid, + makers.make_box( + item.parent, + layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS, + ), + ) + if item.uuid in (i.id for i in parent_box.children): + continue + box = makers.make_box(item) + parent_box.children.append(box) + + for elem in item.elements: + if elem.abstract_type: + self.classes.setdefault( + elem.abstract_type.uuid, elem.abstract_type + ) + self.data.edges.append( + _elkjs.ELKInputEdge( + id=f"__ExchangeItemElement:{elem.uuid}", + sources=[item.uuid], + targets=[elem.abstract_type.uuid], + labels=makers.make_label(elem.name), + ) + ) + for cls in self.classes.values(): + self.global_boxes.setdefault(cls.uuid, self._make_class_box(cls)) + if cls.super and cls.super.uuid in self.classes: + self.data.edges.append( + _elkjs.ELKInputEdge( + id=f"__Generalization:{cls.uuid}", + sources=[cls.uuid], + targets=[cls.super.uuid], + ) + ) + if not self.global_boxes: + logger.warning("Nothing to see here") + return + self.data.children.extend(self.global_boxes.values()) + + def _make_class_box(self, cls: information.Class) -> _elkjs.ELKInputChild: + box = makers.make_box( + cls, layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS + ) + properties = [ + _elkjs.ELKInputLabel( + text="", + layoutOptions=makers.DEFAULT_LABEL_LAYOUT_OPTIONS, + width=0, + height=0, + ) + ] + for prop in cls.properties: + if ( + prop.type is None + or ( + isinstance(prop.type, information.Class) + or isinstance(prop.type, information.datatype.Enumeration) + ) + and prop.type.is_primitive + ): + continue + + text = tree_view._get_property_text(prop) + labels = makers.make_label( + text, + icon=(makers.ICON_WIDTH, 0), + layout_options=tree_view.DATA_TYPE_LABEL_LAYOUT_OPTIONS, + max_width=math.inf, + ) + properties.extend(labels) + + box.labels.extend(properties) + box.width, box.height = makers.calculate_height_and_width(properties) + + return box + + +def collector( + diagram: context.ElementRelationViewDiagram, params: dict[str, t.Any] +) -> _elkjs.ELKInputData: + data = makers.make_diagram(diagram) + # data.layoutOptions = DEFAULT_LAYOUT_OPTIONS + processor = ElementRelationProcessor(diagram, data, params=params) + processor.process() + return data diff --git a/capellambse_context_diagrams/context.py b/capellambse_context_diagrams/context.py index 7bd7c820..919670ef 100644 --- a/capellambse_context_diagrams/context.py +++ b/capellambse_context_diagrams/context.py @@ -19,6 +19,7 @@ from . import _elkjs, filters, serializers, styling from .collectors import ( dataflow_view, + element_relation_view, exchanges, get_elkdata, realization_view, @@ -124,13 +125,6 @@ def __get__( # type: ignore class ClassTreeAccessor(ContextAccessor): """Provides access to the tree view diagrams.""" - # pylint: disable=super-init-not-called - def __init__( - self, diagclass: str, render_params: dict[str, t.Any] | None = None - ) -> None: - self._dgcls = diagclass - self._default_render_params = render_params or {} - def __get__( # type: ignore self, obj: common.T | None, @@ -147,13 +141,6 @@ def __get__( # type: ignore class RealizationViewContextAccessor(ContextAccessor): """Provides access to the realization view diagrams.""" - # pylint: disable=super-init-not-called - def __init__( - self, diagclass: str, render_params: dict[str, t.Any] | None = None - ) -> None: - self._dgcls = diagclass - self._default_render_params = render_params or {} - def __get__( # type: ignore self, obj: common.T | None, @@ -168,12 +155,6 @@ def __get__( # type: ignore class DataFlowAccessor(ContextAccessor): - # pylint: disable=super-init-not-called - def __init__( - self, diagclass: str, render_params: dict[str, t.Any] | None = None - ) -> None: - self._dgcls = diagclass - self._default_render_params = render_params or {} def __get__( # type: ignore self, @@ -188,6 +169,20 @@ def __get__( # type: ignore return self._get(obj, DataFlowViewDiagram) +class ElementRelationAccessor(ContextAccessor): + def __get__( # type: ignore + self, + obj: common.T | None, + objtype: type | None = None, + ) -> common.Accessor | ContextDiagram: + """Make a ElementRelationViewDiagram for the given model object.""" + del objtype + if obj is None: # pragma: no cover + return self + assert isinstance(obj, common.GenericElement) + return self._get(obj, ElementRelationViewDiagram) + + class ContextDiagram(diagram.AbstractDiagram): """An automatically generated context diagram. @@ -675,6 +670,26 @@ def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram: return super()._create_diagram(params) +class ElementRelationViewDiagram(ContextDiagram): + + @property + def uuid(self) -> str: # type: ignore + """Returns the UUID of the diagram.""" + return f"{self.target.uuid}_element_relation_view" + + @property + def name(self) -> str: # type: ignore + return f"Element Relation View of {self.target.name}" + + def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram: + data = element_relation_view.collector(self, params) + layout = try_to_layout(data) + return self.serializer.make_diagram( + layout, + transparent_background=params.get("transparent_background", False), + ) + + def try_to_layout(data: _elkjs.ELKInputData) -> _elkjs.ELKOutputData: """Try calling elkjs, raise a JSONDecodeError if it fails.""" try: From c2b17f3bfe8f3889958493d8311766b28938e428 Mon Sep 17 00:00:00 2001 From: huyenngn Date: Tue, 23 Jul 2024 21:45:21 +0000 Subject: [PATCH 02/12] test(element-relation): Add test data --- tests/data/ContextDiagram.aird | 690 ++++++++++++++++++++++++++++++ tests/data/ContextDiagram.capella | 153 ++++++- 2 files changed, 842 insertions(+), 1 deletion(-) diff --git a/tests/data/ContextDiagram.aird b/tests/data/ContextDiagram.aird index e0515cdb..371f3533 100644 --- a/tests/data/ContextDiagram.aird +++ b/tests/data/ContextDiagram.aird @@ -12,6 +12,10 @@ + + + + @@ -154,6 +158,14 @@ + + +
+
+ + + + @@ -15230,4 +15242,682 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/ContextDiagram.capella b/tests/data/ContextDiagram.capella index fe7ed768..2b02971c 100644 --- a/tests/data/ContextDiagram.capella +++ b/tests/data/ContextDiagram.capella @@ -62,7 +62,7 @@ definition="#682bd51d-5451-4930-a97e-8bfca6c3a127" value="true"/> + value="2021-07-23T13:00:00.000+0000"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id="276e5728-52d2-4819-bb2f-b36cd0f35bd6" targetElement="#cb1e41b1-744b-43ea-89d2-d7c54dff4606" sourceElement="#2f8ed849-fbda-4902-82ec-cbf8104ae686"/> + name="Derived Target" abstractType="#7cde329d-0f47-4eaa-a1ae-dc6ede90af57"/> + + @@ -4321,6 +4460,18 @@ The predator is far away id="1f1545b8-04b8-4bd1-9009-c9d514155660" targetElement="#8b3b14d5-ed06-48fe-acd7-354657944e54" sourceElement="#aad1bc27-e66c-4b38-a54a-b2de4a73a3b4"/> + + + + + + Date: Wed, 24 Jul 2024 00:09:29 +0200 Subject: [PATCH 03/12] test(element-relation): Add tests --- .../collectors/element_relation_view.py | 24 ++++++++----------- tests/test_element_relation_views.py | 22 +++++++++++++++++ 2 files changed, 32 insertions(+), 14 deletions(-) create mode 100644 tests/test_element_relation_views.py diff --git a/capellambse_context_diagrams/collectors/element_relation_view.py b/capellambse_context_diagrams/collectors/element_relation_view.py index edaab36a..4b8ea32a 100644 --- a/capellambse_context_diagrams/collectors/element_relation_view.py +++ b/capellambse_context_diagrams/collectors/element_relation_view.py @@ -17,12 +17,10 @@ logger = logging.getLogger(__name__) DEFAULT_LAYOUT_OPTIONS: _elkjs.LayoutOptions = { - "algorithm": "sporeOverlap", - "elk.underlyingLayoutAlgorithm": "radial", + "algorithm": "radial", "elk.spacing.nodeNode": 20, "elk.spacing.edgeNode": 10, "elk.edgeLabels.inline": True, - "elk.radial.compactor": "WEDGE_COMPACTION", } @@ -60,9 +58,10 @@ def process(self) -> None: self.classes.setdefault( elem.abstract_type.uuid, elem.abstract_type ) + eid = f"__ExchangeItemElement:{item.uuid}_{elem.abstract_type.uuid}" self.data.edges.append( _elkjs.ELKInputEdge( - id=f"__ExchangeItemElement:{elem.uuid}", + id=eid, sources=[item.uuid], targets=[elem.abstract_type.uuid], labels=makers.make_label(elem.name), @@ -71,11 +70,12 @@ def process(self) -> None: for cls in self.classes.values(): self.global_boxes.setdefault(cls.uuid, self._make_class_box(cls)) if cls.super and cls.super.uuid in self.classes: + eid = f"__Generalization:{cls.super.uuid}_{cls.uuid}" self.data.edges.append( _elkjs.ELKInputEdge( - id=f"__Generalization:{cls.uuid}", - sources=[cls.uuid], - targets=[cls.super.uuid], + id=eid, + sources=[cls.super.uuid], + targets=[cls.uuid], ) ) if not self.global_boxes: @@ -96,13 +96,9 @@ def _make_class_box(self, cls: information.Class) -> _elkjs.ELKInputChild: ) ] for prop in cls.properties: - if ( - prop.type is None - or ( - isinstance(prop.type, information.Class) - or isinstance(prop.type, information.datatype.Enumeration) - ) - and prop.type.is_primitive + if prop.type is None or ( + isinstance(prop.type, information.Class) + and not prop.type.is_primitive ): continue diff --git a/tests/test_element_relation_views.py b/tests/test_element_relation_views.py new file mode 100644 index 00000000..8fef902a --- /dev/null +++ b/tests/test_element_relation_views.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2022 Copyright DB InfraGO AG and the capellambse-context-diagrams contributors +# SPDX-License-Identifier: Apache-2.0 + +import capellambse +import pytest + + +@pytest.mark.parametrize( + "uuid", + [ + pytest.param("0ab202d7-6497-4b78-9d13-fd7c9a75486c", id="LA"), + ], +) +def test_element_relation_views( + model: capellambse.MelodyModel, uuid: str +) -> None: + obj = model.by_uuid(uuid) + + diag = obj.element_relation_view + diag.render("svgdiagram").save(pretty=True) + + assert True From aad735fac0b86ef99784fbc04c78b526f018f9e5 Mon Sep 17 00:00:00 2001 From: huyenngn Date: Wed, 24 Jul 2024 13:10:30 +0200 Subject: [PATCH 04/12] feat: Add hidden elements --- .../collectors/element_relation_view.py | 63 +++++++++++++++---- capellambse_context_diagrams/serializers.py | 8 +++ 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/capellambse_context_diagrams/collectors/element_relation_view.py b/capellambse_context_diagrams/collectors/element_relation_view.py index 4b8ea32a..4b5b2679 100644 --- a/capellambse_context_diagrams/collectors/element_relation_view.py +++ b/capellambse_context_diagrams/collectors/element_relation_view.py @@ -7,20 +7,33 @@ import math import typing as t -from capellambse.model import common -from capellambse.model.crosslayer import cs, fa, information -from capellambse.model.modeltypes import DiagramType as DT +from capellambse.model.crosslayer import information from .. import _elkjs, context -from . import generic, makers, tree_view +from . import makers, tree_view logger = logging.getLogger(__name__) DEFAULT_LAYOUT_OPTIONS: _elkjs.LayoutOptions = { - "algorithm": "radial", - "elk.spacing.nodeNode": 20, - "elk.spacing.edgeNode": 10, - "elk.edgeLabels.inline": True, + "algorithm": "layered", + "edgeRouting": "ORTHOGONAL", + "elk.direction": "RIGHT", + "hierarchyHandling": "INCLUDE_CHILDREN", + "layered.edgeLabels.sideSelection": "SMART_DOWN", + "layered.nodePlacement.strategy": "NETWORK_SIMPLEX", + "spacing.labelNode": "0.0", + "partitioning.active": "True", + "nodeSize.constraints": "NODE_LABELS", +} + +DEFAULT_TREE_VIEW_LAYOUT_OPTIONS: _elkjs.LayoutOptions = { + "layered.edgeLabels.sideSelection": "ALWAYS_DOWN", + "algorithm": "layered", + "elk.direction": "DOWN", + "edgeRouting": "POLYLINE", + "nodeSize.constraints": "NODE_LABELS", + "partitioning.active": "True", + "partitioning.partition": 1, } @@ -35,7 +48,14 @@ def __init__( self.diagram = diagram self.data = data self.params = params or {} + self.tree_view_box = _elkjs.ELKInputChild( + id="__HideElement:tree-view", + layoutOptions=DEFAULT_TREE_VIEW_LAYOUT_OPTIONS, + children=[], + edges=[], + ) self.global_boxes: dict[str, _elkjs.ELKInputChild] = {} + self.stack_height = 0.0 self.classes: dict[str, information.Class] = {} def process(self) -> None: @@ -48,6 +68,9 @@ def process(self) -> None: layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS, ), ) + parent_box.layoutOptions["partitioning.partition"] = 0 + self.stack_height += parent_box.height + if item.uuid in (i.id for i in parent_box.children): continue box = makers.make_box(item) @@ -68,16 +91,32 @@ def process(self) -> None: ) ) for cls in self.classes.values(): - self.global_boxes.setdefault(cls.uuid, self._make_class_box(cls)) - if cls.super and cls.super.uuid in self.classes: + box = self._make_class_box(cls) + partition = 0 + current = cls + while current.super and current.super.uuid in self.classes: + partition += 1 + current = current.super + box.layoutOptions["partitioning.partition"] = partition + self.tree_view_box.children.append(box) + if partition > 0: eid = f"__Generalization:{cls.super.uuid}_{cls.uuid}" - self.data.edges.append( + self.tree_view_box.edges.append( _elkjs.ELKInputEdge( id=eid, sources=[cls.super.uuid], targets=[cls.uuid], ) ) + + mid_height = self.stack_height / 2 + for box in self.global_boxes.values(): + if self.stack_height < mid_height: + break + box.layoutOptions["partitioning.partition"] = 2 + self.stack_height -= box.height + + self.global_boxes["__HideElement:tree-view"] = self.tree_view_box if not self.global_boxes: logger.warning("Nothing to see here") return @@ -121,7 +160,7 @@ def collector( diagram: context.ElementRelationViewDiagram, params: dict[str, t.Any] ) -> _elkjs.ELKInputData: data = makers.make_diagram(diagram) - # data.layoutOptions = DEFAULT_LAYOUT_OPTIONS + data.layoutOptions = DEFAULT_LAYOUT_OPTIONS processor = ElementRelationProcessor(diagram, data, params=params) processor.process() return data diff --git a/capellambse_context_diagrams/serializers.py b/capellambse_context_diagrams/serializers.py index 4df27002..126d0f7c 100644 --- a/capellambse_context_diagrams/serializers.py +++ b/capellambse_context_diagrams/serializers.py @@ -131,6 +131,14 @@ class type that stores all previously named classes. uuid: str styleclass: str | None derived = False + + if child.id.startswith("__HideElement"): + if hasattr(child, "position"): + ref = ref + (child.position.x, child.position.y) + for i in getattr(child, "children", []): + self.deserialize_child(i, ref, parent) + return + if child.id.startswith("__"): if ":" in child.id: styleclass, uuid = child.id[2:].split(":", 1) From c7b15ed6a75a989e05c7498d76d90216e27a23f3 Mon Sep 17 00:00:00 2001 From: huyenngn Date: Thu, 1 Aug 2024 14:30:02 +0200 Subject: [PATCH 05/12] fix: Fix collector for ex item elements --- capellambse_context_diagrams/_elkjs.py | 1 + .../collectors/element_relation_view.py | 66 ++++++++++++------- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/capellambse_context_diagrams/_elkjs.py b/capellambse_context_diagrams/_elkjs.py index 8888c20b..527d49b4 100644 --- a/capellambse_context_diagrams/_elkjs.py +++ b/capellambse_context_diagrams/_elkjs.py @@ -147,6 +147,7 @@ class ELKInputEdge(BaseELKModel): """Exchange data that can be fed to ELK.""" id: str + layoutOptions: LayoutOptions = pydantic.Field(default_factory=dict) sources: cabc.MutableSequence[str] targets: cabc.MutableSequence[str] labels: cabc.MutableSequence[ELKInputLabel] = pydantic.Field( diff --git a/capellambse_context_diagrams/collectors/element_relation_view.py b/capellambse_context_diagrams/collectors/element_relation_view.py index 4b5b2679..61211ac2 100644 --- a/capellambse_context_diagrams/collectors/element_relation_view.py +++ b/capellambse_context_diagrams/collectors/element_relation_view.py @@ -16,24 +16,27 @@ DEFAULT_LAYOUT_OPTIONS: _elkjs.LayoutOptions = { "algorithm": "layered", - "edgeRouting": "ORTHOGONAL", - "elk.direction": "RIGHT", - "hierarchyHandling": "INCLUDE_CHILDREN", - "layered.edgeLabels.sideSelection": "SMART_DOWN", "layered.nodePlacement.strategy": "NETWORK_SIMPLEX", "spacing.labelNode": "0.0", - "partitioning.active": "True", "nodeSize.constraints": "NODE_LABELS", + "aspectRatio": 1000, + "layered.considerModelOrder.strategy": "PREFER_NODES", + "layered.considerModelOrder.components": "MODEL_ORDER", + "edgeRouting": "ORTHOGONAL", +} + +DEFAULT_EDGE_LAYOUT_OPTIONS: _elkjs.LayoutOptions = { + "spacing.edgeLabel": "0.0", + "layered.edgeLabels.sideSelection": "SMART_DOWN", } DEFAULT_TREE_VIEW_LAYOUT_OPTIONS: _elkjs.LayoutOptions = { - "layered.edgeLabels.sideSelection": "ALWAYS_DOWN", "algorithm": "layered", "elk.direction": "DOWN", + "layered.edgeLabels.sideSelection": "ALWAYS_DOWN", "edgeRouting": "POLYLINE", "nodeSize.constraints": "NODE_LABELS", - "partitioning.active": "True", - "partitioning.partition": 1, + "partitioning.active": True, } @@ -41,12 +44,12 @@ class ElementRelationProcessor: def __init__( self, diagram: context.ElementRelationViewDiagram, - data: _elkjs.ELKInputData, *, params: dict[str, t.Any] | None = None, ) -> None: self.diagram = diagram - self.data = data + self.data = makers.make_diagram(diagram) + self.data.layoutOptions = DEFAULT_LAYOUT_OPTIONS self.params = params or {} self.tree_view_box = _elkjs.ELKInputChild( id="__HideElement:tree-view", @@ -54,8 +57,20 @@ def __init__( children=[], edges=[], ) + self.left_box = _elkjs.ELKInputChild( + id="__HideElement:left", + layoutOptions=_elkjs.get_global_layered_layout_options(), + children=[], + edges=[], + ) + self.right_box = _elkjs.ELKInputChild( + id="__HideElement:right", + layoutOptions=_elkjs.get_global_layered_layout_options(), + children=[], + edges=[], + ) + self.left_boxes_n = 0 self.global_boxes: dict[str, _elkjs.ELKInputChild] = {} - self.stack_height = 0.0 self.classes: dict[str, information.Class] = {} def process(self) -> None: @@ -68,13 +83,13 @@ def process(self) -> None: layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS, ), ) - parent_box.layoutOptions["partitioning.partition"] = 0 - self.stack_height += parent_box.height + self.left_box.children.append(parent_box) if item.uuid in (i.id for i in parent_box.children): continue box = makers.make_box(item) parent_box.children.append(box) + self.left_boxes_n += 1 for elem in item.elements: if elem.abstract_type: @@ -85,6 +100,7 @@ def process(self) -> None: self.data.edges.append( _elkjs.ELKInputEdge( id=eid, + layoutOptions=DEFAULT_EDGE_LAYOUT_OPTIONS, sources=[item.uuid], targets=[elem.abstract_type.uuid], labels=makers.make_label(elem.name), @@ -109,18 +125,20 @@ def process(self) -> None: ) ) - mid_height = self.stack_height / 2 - for box in self.global_boxes.values(): - if self.stack_height < mid_height: - break - box.layoutOptions["partitioning.partition"] = 2 - self.stack_height -= box.height + right_boxes_n = 0 + while self.left_boxes_n > right_boxes_n: + box = self.left_box.children.pop() + self.right_box.children.append(box) + n = len(box.children) + self.left_boxes_n -= n + right_boxes_n += n - self.global_boxes["__HideElement:tree-view"] = self.tree_view_box if not self.global_boxes: logger.warning("Nothing to see here") return - self.data.children.extend(self.global_boxes.values()) + self.data.children.extend( + [self.left_box, self.tree_view_box, self.right_box] + ) def _make_class_box(self, cls: information.Class) -> _elkjs.ELKInputChild: box = makers.make_box( @@ -159,8 +177,6 @@ def _make_class_box(self, cls: information.Class) -> _elkjs.ELKInputChild: def collector( diagram: context.ElementRelationViewDiagram, params: dict[str, t.Any] ) -> _elkjs.ELKInputData: - data = makers.make_diagram(diagram) - data.layoutOptions = DEFAULT_LAYOUT_OPTIONS - processor = ElementRelationProcessor(diagram, data, params=params) + processor = ElementRelationProcessor(diagram, params=params) processor.process() - return data + return processor.data From 88e15a1ca73940f35d343992d320a44440c8fb22 Mon Sep 17 00:00:00 2001 From: huyenngn Date: Fri, 2 Aug 2024 10:23:47 +0200 Subject: [PATCH 06/12] fix: Omit exchange item element labels Because of elkjs limitations edge labels cannot be displayed properly at this point without sacrificing the desired layout. Edge labels will be omitted completely as to not create confusion with them showing up on the top left corner. They should be added back when the issues with the edge labels got fixed. --- .../collectors/element_relation_view.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/capellambse_context_diagrams/collectors/element_relation_view.py b/capellambse_context_diagrams/collectors/element_relation_view.py index 61211ac2..bd7ef9f3 100644 --- a/capellambse_context_diagrams/collectors/element_relation_view.py +++ b/capellambse_context_diagrams/collectors/element_relation_view.py @@ -103,7 +103,8 @@ def process(self) -> None: layoutOptions=DEFAULT_EDGE_LAYOUT_OPTIONS, sources=[item.uuid], targets=[elem.abstract_type.uuid], - labels=makers.make_label(elem.name), + # Add back labels once edge label issue is fixed + # labels=makers.make_label(elem.name), ) ) for cls in self.classes.values(): From 84aa5c405eda361eadb73b6a57c63f7101ff36ef Mon Sep 17 00:00:00 2001 From: Huyen Nguyen <48179958+huyenngn@users.noreply.github.com> Date: Mon, 12 Aug 2024 12:17:18 +0200 Subject: [PATCH 07/12] docs: Add docstrings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernst Würger --- .../collectors/element_relation_view.py | 1 + capellambse_context_diagrams/context.py | 2 +- tests/test_element_relation_views.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/capellambse_context_diagrams/collectors/element_relation_view.py b/capellambse_context_diagrams/collectors/element_relation_view.py index bd7ef9f3..54f89696 100644 --- a/capellambse_context_diagrams/collectors/element_relation_view.py +++ b/capellambse_context_diagrams/collectors/element_relation_view.py @@ -178,6 +178,7 @@ def _make_class_box(self, cls: information.Class) -> _elkjs.ELKInputChild: def collector( diagram: context.ElementRelationViewDiagram, params: dict[str, t.Any] ) -> _elkjs.ELKInputData: + """Return ExchangeElement data for ELK.""" processor = ElementRelationProcessor(diagram, params=params) processor.process() return processor.data diff --git a/capellambse_context_diagrams/context.py b/capellambse_context_diagrams/context.py index 919670ef..dea80d96 100644 --- a/capellambse_context_diagrams/context.py +++ b/capellambse_context_diagrams/context.py @@ -671,7 +671,7 @@ def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram: class ElementRelationViewDiagram(ContextDiagram): - + """An automatically generated ExchangeElementRelationViewDiagram.""" @property def uuid(self) -> str: # type: ignore """Returns the UUID of the diagram.""" diff --git a/tests/test_element_relation_views.py b/tests/test_element_relation_views.py index 8fef902a..1205bfb4 100644 --- a/tests/test_element_relation_views.py +++ b/tests/test_element_relation_views.py @@ -19,4 +19,4 @@ def test_element_relation_views( diag = obj.element_relation_view diag.render("svgdiagram").save(pretty=True) - assert True + assert diag.nodes From 64edbcbf59e20219384760f0b0c4755716f3379b Mon Sep 17 00:00:00 2001 From: huyenngn Date: Mon, 12 Aug 2024 16:26:49 +0200 Subject: [PATCH 08/12] fix: Apply review suggestions --- capellambse_context_diagrams/__init__.py | 32 +++++++--------- ...ew.py => exchange_item_class_tree_view.py} | 26 +++++-------- capellambse_context_diagrams/context.py | 37 +++++++++++++++---- tests/test_element_relation_views.py | 22 ----------- tests/test_exchange_item_class_tree_views.py | 18 +++++++++ 5 files changed, 70 insertions(+), 65 deletions(-) rename capellambse_context_diagrams/collectors/{element_relation_view.py => exchange_item_class_tree_view.py} (90%) delete mode 100644 tests/test_element_relation_views.py create mode 100644 tests/test_exchange_item_class_tree_views.py diff --git a/capellambse_context_diagrams/__init__.py b/capellambse_context_diagrams/__init__.py index 026af306..b4ea1404 100644 --- a/capellambse_context_diagrams/__init__.py +++ b/capellambse_context_diagrams/__init__.py @@ -66,7 +66,7 @@ def init() -> None: register_tree_view() register_realization_view() register_data_flow_view() - register_element_relation_view() + register_exchange_item_class_tree_view() # register_functional_context() XXX: Future @@ -275,36 +275,30 @@ def register_data_flow_view() -> None: common.set_accessor(class_, "data_flow_view", accessor) -def register_element_relation_view() -> None: +def register_exchange_item_class_tree_view() -> None: - supported_classes = [ - ( - oa.CommunicationMean, - { - DiagramType.OAB, - }, - ), + supported_classes: list[SupportedClass] = [ + (oa.CommunicationMean, {DiagramType.OAB}, {}), ( fa.ComponentExchange, - { - DiagramType.SAB, - DiagramType.LAB, - DiagramType.PAB, - }, + {DiagramType.SAB, DiagramType.LAB, DiagramType.PAB}, + {}, ), ] styles: dict[str, dict[str, capstyle.CSSdef]] = {} - for _class, _diagclasses in supported_classes: + for _class, _diagclasses, render_params in supported_classes: common.set_accessor( _class, - "element_relation_view", - context.ElementRelationAccessor("ElementRelationView Diagram"), + "exchange_item_class_tree_view", + context.ExchangeItemClassTreeAccessor( + "ExchangeItemClassTreeView Diagram", render_params + ), ) for dgcls in _diagclasses: styles.update(capstyle.STYLES.get(dgcls.value, {})) - capstyle.STYLES["ElementRelationView Diagram"] = styles - capstyle.STYLES["ElementRelationView Diagram"].update( + capstyle.STYLES["ExchangeItemClassTreeView Diagram"] = styles + capstyle.STYLES["ExchangeItemClassTreeView Diagram"].update( capstyle.STYLES["Class Diagram Blank"] ) diff --git a/capellambse_context_diagrams/collectors/element_relation_view.py b/capellambse_context_diagrams/collectors/exchange_item_class_tree_view.py similarity index 90% rename from capellambse_context_diagrams/collectors/element_relation_view.py rename to capellambse_context_diagrams/collectors/exchange_item_class_tree_view.py index 54f89696..dfbedc6c 100644 --- a/capellambse_context_diagrams/collectors/element_relation_view.py +++ b/capellambse_context_diagrams/collectors/exchange_item_class_tree_view.py @@ -40,10 +40,10 @@ } -class ElementRelationProcessor: +class ExchangeItemClassTreeCollector: def __init__( self, - diagram: context.ElementRelationViewDiagram, + diagram: context.ExchangeItemClassTreeViewDiagram, *, params: dict[str, t.Any] | None = None, ) -> None: @@ -73,7 +73,10 @@ def __init__( self.global_boxes: dict[str, _elkjs.ELKInputChild] = {} self.classes: dict[str, information.Class] = {} - def process(self) -> None: + def __call__(self) -> _elkjs.ELKInputData: + if not self.diagram.target.allocated_exchange_items: + logger.warning("No exchange items to display") + return self.data for item in self.diagram.target.allocated_exchange_items: if not (parent_box := self.global_boxes.get(item.parent.uuid)): parent_box = self.global_boxes.setdefault( @@ -126,20 +129,14 @@ def process(self) -> None: ) ) - right_boxes_n = 0 - while self.left_boxes_n > right_boxes_n: + while len(self.left_box.children) > len(self.right_box.children): box = self.left_box.children.pop() self.right_box.children.append(box) - n = len(box.children) - self.left_boxes_n -= n - right_boxes_n += n - if not self.global_boxes: - logger.warning("Nothing to see here") - return self.data.children.extend( [self.left_box, self.tree_view_box, self.right_box] ) + return self.data def _make_class_box(self, cls: information.Class) -> _elkjs.ELKInputChild: box = makers.make_box( @@ -171,14 +168,11 @@ def _make_class_box(self, cls: information.Class) -> _elkjs.ELKInputChild: box.labels.extend(properties) box.width, box.height = makers.calculate_height_and_width(properties) - return box def collector( - diagram: context.ElementRelationViewDiagram, params: dict[str, t.Any] + diagram: context.ExchangeItemClassTreeViewDiagram, params: dict[str, t.Any] ) -> _elkjs.ELKInputData: """Return ExchangeElement data for ELK.""" - processor = ElementRelationProcessor(diagram, params=params) - processor.process() - return processor.data + return ExchangeItemClassTreeCollector(diagram, params=params)() diff --git a/capellambse_context_diagrams/context.py b/capellambse_context_diagrams/context.py index dea80d96..6ad00721 100644 --- a/capellambse_context_diagrams/context.py +++ b/capellambse_context_diagrams/context.py @@ -19,7 +19,7 @@ from . import _elkjs, filters, serializers, styling from .collectors import ( dataflow_view, - element_relation_view, + exchange_item_class_tree_view, exchanges, get_elkdata, realization_view, @@ -125,6 +125,13 @@ def __get__( # type: ignore class ClassTreeAccessor(ContextAccessor): """Provides access to the tree view diagrams.""" + # pylint: disable=super-init-not-called + def __init__( + self, diagclass: str, render_params: dict[str, t.Any] | None = None + ) -> None: + self._dgcls = diagclass + self._default_render_params = render_params or {} + def __get__( # type: ignore self, obj: common.T | None, @@ -141,6 +148,13 @@ def __get__( # type: ignore class RealizationViewContextAccessor(ContextAccessor): """Provides access to the realization view diagrams.""" + # pylint: disable=super-init-not-called + def __init__( + self, diagclass: str, render_params: dict[str, t.Any] | None = None + ) -> None: + self._dgcls = diagclass + self._default_render_params = render_params or {} + def __get__( # type: ignore self, obj: common.T | None, @@ -155,6 +169,12 @@ def __get__( # type: ignore class DataFlowAccessor(ContextAccessor): + # pylint: disable=super-init-not-called + def __init__( + self, diagclass: str, render_params: dict[str, t.Any] | None = None + ) -> None: + self._dgcls = diagclass + self._default_render_params = render_params or {} def __get__( # type: ignore self, @@ -169,18 +189,18 @@ def __get__( # type: ignore return self._get(obj, DataFlowViewDiagram) -class ElementRelationAccessor(ContextAccessor): +class ExchangeItemClassTreeAccessor(ContextAccessor): def __get__( # type: ignore self, obj: common.T | None, objtype: type | None = None, ) -> common.Accessor | ContextDiagram: - """Make a ElementRelationViewDiagram for the given model object.""" + """Make a ExchangeItemClassTreeViewDiagram for the given model object.""" del objtype if obj is None: # pragma: no cover return self assert isinstance(obj, common.GenericElement) - return self._get(obj, ElementRelationViewDiagram) + return self._get(obj, ExchangeItemClassTreeViewDiagram) class ContextDiagram(diagram.AbstractDiagram): @@ -670,19 +690,20 @@ def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram: return super()._create_diagram(params) -class ElementRelationViewDiagram(ContextDiagram): - """An automatically generated ExchangeElementRelationViewDiagram.""" +class ExchangeItemClassTreeViewDiagram(ContextDiagram): + """An automatically generated ExchangeExchangeItemClassTreeViewDiagram.""" + @property def uuid(self) -> str: # type: ignore """Returns the UUID of the diagram.""" - return f"{self.target.uuid}_element_relation_view" + return f"{self.target.uuid}_exchange_item_class_tree_view" @property def name(self) -> str: # type: ignore return f"Element Relation View of {self.target.name}" def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram: - data = element_relation_view.collector(self, params) + data = exchange_item_class_tree_view.collector(self, params) layout = try_to_layout(data) return self.serializer.make_diagram( layout, diff --git a/tests/test_element_relation_views.py b/tests/test_element_relation_views.py deleted file mode 100644 index 1205bfb4..00000000 --- a/tests/test_element_relation_views.py +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Copyright DB InfraGO AG and the capellambse-context-diagrams contributors -# SPDX-License-Identifier: Apache-2.0 - -import capellambse -import pytest - - -@pytest.mark.parametrize( - "uuid", - [ - pytest.param("0ab202d7-6497-4b78-9d13-fd7c9a75486c", id="LA"), - ], -) -def test_element_relation_views( - model: capellambse.MelodyModel, uuid: str -) -> None: - obj = model.by_uuid(uuid) - - diag = obj.element_relation_view - diag.render("svgdiagram").save(pretty=True) - - assert diag.nodes diff --git a/tests/test_exchange_item_class_tree_views.py b/tests/test_exchange_item_class_tree_views.py new file mode 100644 index 00000000..8eec9806 --- /dev/null +++ b/tests/test_exchange_item_class_tree_views.py @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2022 Copyright DB InfraGO AG and the capellambse-context-diagrams contributors +# SPDX-License-Identifier: Apache-2.0 + +import capellambse +import pytest + + +@pytest.mark.parametrize("uuid", ["0ab202d7-6497-4b78-9d13-fd7c9a75486c"]) +@pytest.mark.parametrize("fmt", ["svgdiagram", "svg", None]) +def test_exchange_item_class_tree_views( + model: capellambse.MelodyModel, uuid: str, fmt: str +) -> None: + obj = model.by_uuid(uuid) + + diag = obj.exchange_item_class_tree_view + diag.render("svgdiagram").save(pretty=True) + + assert diag.render(fmt) From 84ebc9af24b841e6dfe5715746a79a2799a23452 Mon Sep 17 00:00:00 2001 From: huyenngn Date: Tue, 13 Aug 2024 10:28:27 +0200 Subject: [PATCH 09/12] docs: Add docs and images --- capellambse_context_diagrams/__init__.py | 4 ++- capellambse_context_diagrams/context.py | 4 +-- docs/exchange_item_class_tree_view.md | 34 ++++++++++++++++++++ docs/gen_images.py | 9 ++++++ mkdocs.yml | 2 ++ tests/test_exchange_item_class_tree_views.py | 1 - 6 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 docs/exchange_item_class_tree_view.md diff --git a/capellambse_context_diagrams/__init__.py b/capellambse_context_diagrams/__init__.py index b4ea1404..555f7dda 100644 --- a/capellambse_context_diagrams/__init__.py +++ b/capellambse_context_diagrams/__init__.py @@ -277,7 +277,9 @@ def register_data_flow_view() -> None: def register_exchange_item_class_tree_view() -> None: - supported_classes: list[SupportedClass] = [ + supported_classes: list[ + tuple[type[common.GenericElement], set[DiagramType], dict[str, t.Any]] + ] = [ (oa.CommunicationMean, {DiagramType.OAB}, {}), ( fa.ComponentExchange, diff --git a/capellambse_context_diagrams/context.py b/capellambse_context_diagrams/context.py index 6ad00721..5ab87779 100644 --- a/capellambse_context_diagrams/context.py +++ b/capellambse_context_diagrams/context.py @@ -691,7 +691,7 @@ def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram: class ExchangeItemClassTreeViewDiagram(ContextDiagram): - """An automatically generated ExchangeExchangeItemClassTreeViewDiagram.""" + """An automatically generated ExchangeItemClassTreeViewDiagram.""" @property def uuid(self) -> str: # type: ignore @@ -700,7 +700,7 @@ def uuid(self) -> str: # type: ignore @property def name(self) -> str: # type: ignore - return f"Element Relation View of {self.target.name}" + return f"Exchange Item Class Tree View of {self.target.name}" def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram: data = exchange_item_class_tree_view.collector(self, params) diff --git a/docs/exchange_item_class_tree_view.md b/docs/exchange_item_class_tree_view.md new file mode 100644 index 00000000..3467084f --- /dev/null +++ b/docs/exchange_item_class_tree_view.md @@ -0,0 +1,34 @@ + + +# Exchange Item Class Tree View + +The `ExchangeItemClassTreeView` visualizes the hierarchical structure of exchange items and the relationships between their associated classes in a tree view. You can access `.exchange_item_class_tree_view` on any `fa.ComponentExchange`. Data collection starts on the allocated exchange items and collects the associated classes through their exchange item elements. + +??? example "Exchange Item Class Tree View of C 28" + + ``` py + import capellambse + + model = capellambse.MelodyModel("tests/data/ContextDiagram.aird") + diag = model.by_uuid("0ab202d7-6497-4b78-9d13-fd7c9a75486c").exchange_item_class_tree_view + diag.render("svgdiagram").save(pretty=True) + ``` +
+ +
[LAB] Exchange Item Class Tree View of C 28
+
+ +## Known Issues + +One known issue with the current implementation is related to the routing of edges for ExchangeItemElements. The edges might not be routed optimally in certain cases due to the limitations of ELK'S edge routing algorithms. + +This issue could potentially be resolved when Libavoid for ELK becomes publicly available. Libavoid is an advanced edge routing library that offers object-avoiding orthogonal and polyline connector routing, which could improve the layout of the edges in the diagram. At that point the exchange item element labels will be added to the diagram as well. + +## Check out the code + +To understand the collection have a look into the +[`exchange_item_class_tree_view`][capellambse_context_diagrams.collectors.exchange_item_class_tree_view] +module. diff --git a/docs/gen_images.py b/docs/gen_images.py index 1e699aca..2fcb18b2 100644 --- a/docs/gen_images.py +++ b/docs/gen_images.py @@ -42,6 +42,7 @@ realization_comp_uuid = "b9f9a83c-fb02-44f7-9123-9d86326de5f1" data_flow_uuid = "3b83b4ba-671a-4de8-9c07-a5c6b1d3c422" derived_uuid = "47c3130b-ec39-4365-a77a-5ab6365d1e2e" +exchange_item_class_tree_uuid = "0ab202d7-6497-4b78-9d13-fd7c9a75486c" def generate_index_images() -> None: @@ -192,6 +193,13 @@ def generate_interface_with_hide_interface_image(): print(diag.render("svg", **params), file=fd) +def generate_exchange_item_class_tree_images() -> None: + obj = model.by_uuid(exchange_item_class_tree_uuid) + diag = obj.exchange_item_class_tree_view + with mkdocs_gen_files.open(f"{str(dest / diag.name)}.svg", "w") as fd: + print(diag.render("svg", transparent_background=False), file=fd) + + generate_index_images() generate_hierarchy_image() generate_symbol_images() @@ -219,3 +227,4 @@ def generate_interface_with_hide_interface_image(): generate_derived_image() generate_interface_with_hide_functions_image() generate_interface_with_hide_interface_image() +generate_exchange_item_class_tree_images() diff --git a/mkdocs.yml b/mkdocs.yml index c3748d25..4d812f7c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -94,6 +94,8 @@ nav: - Overview: realization_view.md - DataFlow View: - Overview: data_flow_view.md + - Exchange Item Class Tree View: + - Overview: exchange_item_class_tree_view.md - Extras: - Filters: extras/filters.md - Styling: extras/styling.md diff --git a/tests/test_exchange_item_class_tree_views.py b/tests/test_exchange_item_class_tree_views.py index 8eec9806..314f5168 100644 --- a/tests/test_exchange_item_class_tree_views.py +++ b/tests/test_exchange_item_class_tree_views.py @@ -13,6 +13,5 @@ def test_exchange_item_class_tree_views( obj = model.by_uuid(uuid) diag = obj.exchange_item_class_tree_view - diag.render("svgdiagram").save(pretty=True) assert diag.render(fmt) From 70c3669172d209ac2d62d92c64d02066fbcbe07a Mon Sep 17 00:00:00 2001 From: huyenngn Date: Wed, 21 Aug 2024 11:55:38 +0200 Subject: [PATCH 10/12] refactor: Rename to exchange item relation view --- capellambse_context_diagrams/__init__.py | 14 +++++++------- ..._view.py => exchange_item_relation_view.py} | 8 ++++---- capellambse_context_diagrams/context.py | 18 +++++++++--------- ..._view.md => exchange_item_relation_view.md} | 14 +++++++------- docs/gen_images.py | 2 +- mkdocs.yml | 4 ++-- ...py => test_exchange_item_relation_views.py} | 4 ++-- 7 files changed, 32 insertions(+), 32 deletions(-) rename capellambse_context_diagrams/collectors/{exchange_item_class_tree_view.py => exchange_item_relation_view.py} (96%) rename docs/{exchange_item_class_tree_view.md => exchange_item_relation_view.md} (59%) rename tests/{test_exchange_item_class_tree_views.py => test_exchange_item_relation_views.py} (83%) diff --git a/capellambse_context_diagrams/__init__.py b/capellambse_context_diagrams/__init__.py index 555f7dda..f10b92cb 100644 --- a/capellambse_context_diagrams/__init__.py +++ b/capellambse_context_diagrams/__init__.py @@ -66,7 +66,7 @@ def init() -> None: register_tree_view() register_realization_view() register_data_flow_view() - register_exchange_item_class_tree_view() + register_exchange_item_relation_view() # register_functional_context() XXX: Future @@ -275,7 +275,7 @@ def register_data_flow_view() -> None: common.set_accessor(class_, "data_flow_view", accessor) -def register_exchange_item_class_tree_view() -> None: +def register_exchange_item_relation_view() -> None: supported_classes: list[ tuple[type[common.GenericElement], set[DiagramType], dict[str, t.Any]] @@ -292,15 +292,15 @@ def register_exchange_item_class_tree_view() -> None: for _class, _diagclasses, render_params in supported_classes: common.set_accessor( _class, - "exchange_item_class_tree_view", - context.ExchangeItemClassTreeAccessor( - "ExchangeItemClassTreeView Diagram", render_params + "exchange_item_relation_view", + context.ExchangeItemRelationAccessor( + "ExchangeItemRelationView Diagram", render_params ), ) for dgcls in _diagclasses: styles.update(capstyle.STYLES.get(dgcls.value, {})) - capstyle.STYLES["ExchangeItemClassTreeView Diagram"] = styles - capstyle.STYLES["ExchangeItemClassTreeView Diagram"].update( + capstyle.STYLES["ExchangeItemRelationView Diagram"] = styles + capstyle.STYLES["ExchangeItemRelationView Diagram"].update( capstyle.STYLES["Class Diagram Blank"] ) diff --git a/capellambse_context_diagrams/collectors/exchange_item_class_tree_view.py b/capellambse_context_diagrams/collectors/exchange_item_relation_view.py similarity index 96% rename from capellambse_context_diagrams/collectors/exchange_item_class_tree_view.py rename to capellambse_context_diagrams/collectors/exchange_item_relation_view.py index dfbedc6c..44e60205 100644 --- a/capellambse_context_diagrams/collectors/exchange_item_class_tree_view.py +++ b/capellambse_context_diagrams/collectors/exchange_item_relation_view.py @@ -40,10 +40,10 @@ } -class ExchangeItemClassTreeCollector: +class ExchangeItemRelationCollector: def __init__( self, - diagram: context.ExchangeItemClassTreeViewDiagram, + diagram: context.ExchangeItemRelationViewDiagram, *, params: dict[str, t.Any] | None = None, ) -> None: @@ -172,7 +172,7 @@ def _make_class_box(self, cls: information.Class) -> _elkjs.ELKInputChild: def collector( - diagram: context.ExchangeItemClassTreeViewDiagram, params: dict[str, t.Any] + diagram: context.ExchangeItemRelationViewDiagram, params: dict[str, t.Any] ) -> _elkjs.ELKInputData: """Return ExchangeElement data for ELK.""" - return ExchangeItemClassTreeCollector(diagram, params=params)() + return ExchangeItemRelationCollector(diagram, params=params)() diff --git a/capellambse_context_diagrams/context.py b/capellambse_context_diagrams/context.py index 5ab87779..4e308481 100644 --- a/capellambse_context_diagrams/context.py +++ b/capellambse_context_diagrams/context.py @@ -19,7 +19,7 @@ from . import _elkjs, filters, serializers, styling from .collectors import ( dataflow_view, - exchange_item_class_tree_view, + exchange_item_relation_view, exchanges, get_elkdata, realization_view, @@ -189,18 +189,18 @@ def __get__( # type: ignore return self._get(obj, DataFlowViewDiagram) -class ExchangeItemClassTreeAccessor(ContextAccessor): +class ExchangeItemRelationAccessor(ContextAccessor): def __get__( # type: ignore self, obj: common.T | None, objtype: type | None = None, ) -> common.Accessor | ContextDiagram: - """Make a ExchangeItemClassTreeViewDiagram for the given model object.""" + """Make a ExchangeItemRelationViewDiagram for the given model object.""" del objtype if obj is None: # pragma: no cover return self assert isinstance(obj, common.GenericElement) - return self._get(obj, ExchangeItemClassTreeViewDiagram) + return self._get(obj, ExchangeItemRelationViewDiagram) class ContextDiagram(diagram.AbstractDiagram): @@ -690,20 +690,20 @@ def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram: return super()._create_diagram(params) -class ExchangeItemClassTreeViewDiagram(ContextDiagram): - """An automatically generated ExchangeItemClassTreeViewDiagram.""" +class ExchangeItemRelationViewDiagram(ContextDiagram): + """An automatically generated ExchangeItemRelationViewDiagram.""" @property def uuid(self) -> str: # type: ignore """Returns the UUID of the diagram.""" - return f"{self.target.uuid}_exchange_item_class_tree_view" + return f"{self.target.uuid}_exchange_item_relation_view" @property def name(self) -> str: # type: ignore - return f"Exchange Item Class Tree View of {self.target.name}" + return f"Exchange Item Relation View of {self.target.name}" def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram: - data = exchange_item_class_tree_view.collector(self, params) + data = exchange_item_relation_view.collector(self, params) layout = try_to_layout(data) return self.serializer.make_diagram( layout, diff --git a/docs/exchange_item_class_tree_view.md b/docs/exchange_item_relation_view.md similarity index 59% rename from docs/exchange_item_class_tree_view.md rename to docs/exchange_item_relation_view.md index 3467084f..3e08ce6d 100644 --- a/docs/exchange_item_class_tree_view.md +++ b/docs/exchange_item_relation_view.md @@ -3,22 +3,22 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -# Exchange Item Class Tree View +# Exchange Item Relation View -The `ExchangeItemClassTreeView` visualizes the hierarchical structure of exchange items and the relationships between their associated classes in a tree view. You can access `.exchange_item_class_tree_view` on any `fa.ComponentExchange`. Data collection starts on the allocated exchange items and collects the associated classes through their exchange item elements. +The `ExchangeItemRelationView` visualizes the hierarchical structure of exchange items and the relationships between their associated classes in a tree view. You can access `.exchange_item_relation_view` on any `fa.ComponentExchange`. Data collection starts on the allocated exchange items and collects the associated classes through their exchange item elements. -??? example "Exchange Item Class Tree View of C 28" +??? example "Exchange Item Relation View of C 28" ``` py import capellambse model = capellambse.MelodyModel("tests/data/ContextDiagram.aird") - diag = model.by_uuid("0ab202d7-6497-4b78-9d13-fd7c9a75486c").exchange_item_class_tree_view + diag = model.by_uuid("0ab202d7-6497-4b78-9d13-fd7c9a75486c").exchange_item_relation_view diag.render("svgdiagram").save(pretty=True) ```
- -
[LAB] Exchange Item Class Tree View of C 28
+ +
[LAB] Exchange Item Relation View of C 28
## Known Issues @@ -30,5 +30,5 @@ This issue could potentially be resolved when Libavoid for ELK becomes publicly ## Check out the code To understand the collection have a look into the -[`exchange_item_class_tree_view`][capellambse_context_diagrams.collectors.exchange_item_class_tree_view] +[`exchange_item_relation_view`][capellambse_context_diagrams.collectors.exchange_item_relation_view] module. diff --git a/docs/gen_images.py b/docs/gen_images.py index 2fcb18b2..7047ecfe 100644 --- a/docs/gen_images.py +++ b/docs/gen_images.py @@ -195,7 +195,7 @@ def generate_interface_with_hide_interface_image(): def generate_exchange_item_class_tree_images() -> None: obj = model.by_uuid(exchange_item_class_tree_uuid) - diag = obj.exchange_item_class_tree_view + diag = obj.exchange_item_relation_view with mkdocs_gen_files.open(f"{str(dest / diag.name)}.svg", "w") as fd: print(diag.render("svg", transparent_background=False), file=fd) diff --git a/mkdocs.yml b/mkdocs.yml index 4d812f7c..c38b8f48 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -94,8 +94,8 @@ nav: - Overview: realization_view.md - DataFlow View: - Overview: data_flow_view.md - - Exchange Item Class Tree View: - - Overview: exchange_item_class_tree_view.md + - Exchange Item Relation View: + - Overview: exchange_item_relation_view.md - Extras: - Filters: extras/filters.md - Styling: extras/styling.md diff --git a/tests/test_exchange_item_class_tree_views.py b/tests/test_exchange_item_relation_views.py similarity index 83% rename from tests/test_exchange_item_class_tree_views.py rename to tests/test_exchange_item_relation_views.py index 314f5168..daf602a8 100644 --- a/tests/test_exchange_item_class_tree_views.py +++ b/tests/test_exchange_item_relation_views.py @@ -7,11 +7,11 @@ @pytest.mark.parametrize("uuid", ["0ab202d7-6497-4b78-9d13-fd7c9a75486c"]) @pytest.mark.parametrize("fmt", ["svgdiagram", "svg", None]) -def test_exchange_item_class_tree_views( +def test_exchange_item_relation_views( model: capellambse.MelodyModel, uuid: str, fmt: str ) -> None: obj = model.by_uuid(uuid) - diag = obj.exchange_item_class_tree_view + diag = obj.exchange_item_relation_view assert diag.render(fmt) From da45d02b542c0ff781a50f51d8b1d079142ee8dc Mon Sep 17 00:00:00 2001 From: huyenngn Date: Tue, 3 Sep 2024 09:25:06 +0200 Subject: [PATCH 11/12] refactor: Remove unused variable --- .../collectors/exchange_item_relation_view.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/capellambse_context_diagrams/collectors/exchange_item_relation_view.py b/capellambse_context_diagrams/collectors/exchange_item_relation_view.py index 44e60205..8fcd058b 100644 --- a/capellambse_context_diagrams/collectors/exchange_item_relation_view.py +++ b/capellambse_context_diagrams/collectors/exchange_item_relation_view.py @@ -69,7 +69,6 @@ def __init__( children=[], edges=[], ) - self.left_boxes_n = 0 self.global_boxes: dict[str, _elkjs.ELKInputChild] = {} self.classes: dict[str, information.Class] = {} @@ -92,7 +91,6 @@ def __call__(self) -> _elkjs.ELKInputData: continue box = makers.make_box(item) parent_box.children.append(box) - self.left_boxes_n += 1 for elem in item.elements: if elem.abstract_type: From c0f6bb0118d9ef96002f4e730fd929dc61f86b17 Mon Sep 17 00:00:00 2001 From: huyenngn Date: Wed, 18 Sep 2024 12:43:09 +0200 Subject: [PATCH 12/12] fix: Fix Styling --- .../collectors/exchange_item_relation_view.py | 114 +++++++----------- capellambse_context_diagrams/serializers.py | 9 +- 2 files changed, 54 insertions(+), 69 deletions(-) diff --git a/capellambse_context_diagrams/collectors/exchange_item_relation_view.py b/capellambse_context_diagrams/collectors/exchange_item_relation_view.py index 8fcd058b..b4594986 100644 --- a/capellambse_context_diagrams/collectors/exchange_item_relation_view.py +++ b/capellambse_context_diagrams/collectors/exchange_item_relation_view.py @@ -16,27 +16,15 @@ DEFAULT_LAYOUT_OPTIONS: _elkjs.LayoutOptions = { "algorithm": "layered", - "layered.nodePlacement.strategy": "NETWORK_SIMPLEX", + "elk.direction": "RIGHT", + "layered.nodePlacement.strategy": "LINEAR_SEGMENTS", + "edgeStraightening": "NONE", "spacing.labelNode": "0.0", - "nodeSize.constraints": "NODE_LABELS", - "aspectRatio": 1000, - "layered.considerModelOrder.strategy": "PREFER_NODES", - "layered.considerModelOrder.components": "MODEL_ORDER", - "edgeRouting": "ORTHOGONAL", -} - -DEFAULT_EDGE_LAYOUT_OPTIONS: _elkjs.LayoutOptions = { - "spacing.edgeLabel": "0.0", - "layered.edgeLabels.sideSelection": "SMART_DOWN", -} - -DEFAULT_TREE_VIEW_LAYOUT_OPTIONS: _elkjs.LayoutOptions = { - "algorithm": "layered", - "elk.direction": "DOWN", "layered.edgeLabels.sideSelection": "ALWAYS_DOWN", "edgeRouting": "POLYLINE", "nodeSize.constraints": "NODE_LABELS", - "partitioning.active": True, + "hierarchyHandling": "INCLUDE_CHILDREN", + "layered.spacing.edgeEdgeBetweenLayers": "2.0", } @@ -51,32 +39,15 @@ def __init__( self.data = makers.make_diagram(diagram) self.data.layoutOptions = DEFAULT_LAYOUT_OPTIONS self.params = params or {} - self.tree_view_box = _elkjs.ELKInputChild( - id="__HideElement:tree-view", - layoutOptions=DEFAULT_TREE_VIEW_LAYOUT_OPTIONS, - children=[], - edges=[], - ) - self.left_box = _elkjs.ELKInputChild( - id="__HideElement:left", - layoutOptions=_elkjs.get_global_layered_layout_options(), - children=[], - edges=[], - ) - self.right_box = _elkjs.ELKInputChild( - id="__HideElement:right", - layoutOptions=_elkjs.get_global_layered_layout_options(), - children=[], - edges=[], - ) self.global_boxes: dict[str, _elkjs.ELKInputChild] = {} self.classes: dict[str, information.Class] = {} + self.edges: dict[str, list[_elkjs.ELKInputEdge]] = {} def __call__(self) -> _elkjs.ELKInputData: - if not self.diagram.target.allocated_exchange_items: + if not self.diagram.target.exchange_items: logger.warning("No exchange items to display") return self.data - for item in self.diagram.target.allocated_exchange_items: + for item in self.diagram.target.exchange_items: if not (parent_box := self.global_boxes.get(item.parent.uuid)): parent_box = self.global_boxes.setdefault( item.parent.uuid, @@ -85,7 +56,6 @@ def __call__(self) -> _elkjs.ELKInputData: layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS, ), ) - self.left_box.children.append(parent_box) if item.uuid in (i.id for i in parent_box.children): continue @@ -93,33 +63,29 @@ def __call__(self) -> _elkjs.ELKInputData: parent_box.children.append(box) for elem in item.elements: - if elem.abstract_type: - self.classes.setdefault( - elem.abstract_type.uuid, elem.abstract_type - ) - eid = f"__ExchangeItemElement:{item.uuid}_{elem.abstract_type.uuid}" - self.data.edges.append( - _elkjs.ELKInputEdge( - id=eid, - layoutOptions=DEFAULT_EDGE_LAYOUT_OPTIONS, - sources=[item.uuid], - targets=[elem.abstract_type.uuid], - # Add back labels once edge label issue is fixed - # labels=makers.make_label(elem.name), - ) - ) + if elem.abstract_type is None: + continue + self.classes.setdefault( + elem.abstract_type.uuid, elem.abstract_type + ) + eid = f"__ExchangeItemElement:{item.uuid}_{elem.abstract_type.uuid}" + edge = _elkjs.ELKInputEdge( + id=eid, + sources=[item.uuid], + targets=[elem.abstract_type.uuid], + labels=makers.make_label( + elem.name, + ), + ) + self.data.edges.append(edge) + self.edges.setdefault(item.parent.uuid, []).append(edge) + for cls in self.classes.values(): box = self._make_class_box(cls) - partition = 0 - current = cls - while current.super and current.super.uuid in self.classes: - partition += 1 - current = current.super - box.layoutOptions["partitioning.partition"] = partition - self.tree_view_box.children.append(box) - if partition > 0: + self.data.children.append(box) + if cls.super: eid = f"__Generalization:{cls.super.uuid}_{cls.uuid}" - self.tree_view_box.edges.append( + self.data.edges.append( _elkjs.ELKInputEdge( id=eid, sources=[cls.super.uuid], @@ -127,13 +93,25 @@ def __call__(self) -> _elkjs.ELKInputData: ) ) - while len(self.left_box.children) > len(self.right_box.children): - box = self.left_box.children.pop() - self.right_box.children.append(box) + top = sum(len(box.children) for box in self.global_boxes.values()) + mid = top / 2 + ordered = list(self.global_boxes.keys()) + for uuid in ordered: + if top < mid: + break + self.data.edges.append( + _elkjs.ELKInputEdge( + id=f"__Hide{uuid}", + sources=[uuid], + targets=[ordered[-1]], + ) + ) + for edge in self.edges.get(uuid, []): + edge.sources, edge.targets = edge.targets, edge.sources + edge.id = f"__Reverse-{edge.id[2:]}" + top -= len(self.global_boxes[uuid].children) - self.data.children.extend( - [self.left_box, self.tree_view_box, self.right_box] - ) + self.data.children.extend(self.global_boxes.values()) return self.data def _make_class_box(self, cls: information.Class) -> _elkjs.ELKInputChild: diff --git a/capellambse_context_diagrams/serializers.py b/capellambse_context_diagrams/serializers.py index 126d0f7c..c0616f9b 100644 --- a/capellambse_context_diagrams/serializers.py +++ b/capellambse_context_diagrams/serializers.py @@ -131,8 +131,9 @@ class type that stores all previously named classes. uuid: str styleclass: str | None derived = False + reverse = False - if child.id.startswith("__HideElement"): + if child.id.startswith("__Hide"): if hasattr(child, "position"): ref = ref + (child.position.x, child.position.y) for i in getattr(child, "children", []): @@ -147,6 +148,9 @@ class type that stores all previously named classes. if styleclass.startswith("Derived-"): styleclass = styleclass.removeprefix("Derived-") derived = True + elif styleclass.startswith("Reverse-"): + styleclass = styleclass.removeprefix("Reverse-") + reverse = True else: styleclass = self.get_styleclass(child.id) uuid = child.id @@ -201,6 +205,9 @@ class type that stores all previously named classes. if target_id.startswith("__"): target_id = target_id[2:].split(":", 1)[-1] + if reverse: + source_id, target_id = target_id, source_id + if child.routingPoints: refpoints = [ ref + (point.x, point.y) for point in child.routingPoints