From c4966ce81bfbfbfe6e2e151d818a63473cb96bf0 Mon Sep 17 00:00:00 2001 From: huyenngn Date: Wed, 25 Sep 2024 09:24:02 +0200 Subject: [PATCH] feat: Implement Cable Tree View --- capellambse_context_diagrams/__init__.py | 13 ++ .../collectors/cable_tree.py | 112 ++++++++++++++++++ capellambse_context_diagrams/context.py | 38 ++++++ 3 files changed, 163 insertions(+) create mode 100644 capellambse_context_diagrams/collectors/cable_tree.py diff --git a/capellambse_context_diagrams/__init__.py b/capellambse_context_diagrams/__init__.py index 7fbbf4e..433313a 100644 --- a/capellambse_context_diagrams/__init__.py +++ b/capellambse_context_diagrams/__init__.py @@ -62,6 +62,7 @@ def init() -> None: register_tree_view() register_realization_view() register_data_flow_view() + register_cable_tree_view() # register_functional_context() XXX: Future @@ -283,3 +284,15 @@ def register_data_flow_view() -> None: for class_, dgcls, default_render_params in supported_classes: accessor = context.DataFlowAccessor(dgcls.value, default_render_params) m.set_accessor(class_, "data_flow_view", accessor) + + +def register_cable_tree_view() -> None: + """Add the `cable_tree_view` attribute to `PhysicalLink`s.""" + m.set_accessor( + cs.PhysicalLink, + "cable_tree", + context.CableTreeAccessor( + DiagramType.PAB.value, + {}, + ), + ) diff --git a/capellambse_context_diagrams/collectors/cable_tree.py b/capellambse_context_diagrams/collectors/cable_tree.py new file mode 100644 index 0000000..23de7f8 --- /dev/null +++ b/capellambse_context_diagrams/collectors/cable_tree.py @@ -0,0 +1,112 @@ +# SPDX-FileCopyrightText: 2022 Copyright DB InfraGO AG and the capellambse-context-diagrams contributors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import copy +import typing as t + +import capellambse.model as m + +from .. import _elkjs, context +from . import makers + +DEFAULT_LAYOUT_OPTIONS: _elkjs.LayoutOptions = { + "algorithm": "layered", + "edgeRouting": "ORTHOGONAL", + "elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX", +} + + +class CableTreeCollector: + def __init__( + self, + diagram: context.ContextDiagram, + params: dict[str, t.Any], + ) -> None: + self.diagram = diagram + self.obj: m.ModelElement = self.diagram.target + self.data = makers.make_diagram(diagram) + self.data.layoutOptions = DEFAULT_LAYOUT_OPTIONS + self.params = params + self.boxes: dict[str, _elkjs.ELKInputChild] = {} + self.edges: dict[str, _elkjs.ELKInputEdge] = {} + self.ports: dict[str, _elkjs.ELKInputPort] = {} + + def __call__(self) -> _elkjs.ELKInputData: + src_obj = self.obj.source + tgt_obj = self.obj.target + target_link = self.make_edge(self.obj, src_obj, tgt_obj) + target_link.layoutOptions = copy.deepcopy( + _elkjs.EDGE_STRAIGHTENING_LAYOUT_OPTIONS + ) + self.make_tree(src_obj) + self.make_tree(tgt_obj, reverse=True) + return self.data + + def make_tree( + self, port_obj: m.ModelElement, reverse: bool = False + ) -> _elkjs.ELKInputChild: + box = self.make_port_and_owner(port_obj) + for link in port_obj.links: + if self.edges.get(link.uuid): + continue + if link.source.uuid == port_obj.uuid: + obj = link.target + else: + obj = link.source + if reverse: + self.make_edge(link, obj, port_obj) + else: + self.make_edge(link, port_obj, obj) + self.make_tree(obj, reverse=reverse) + return box + + def make_edge( + self, + link: m.ModelElement, + src_obj: m.ModelElement, + tgt_obj: m.ModelElement, + ) -> _elkjs.ELKInputEdge: + edge = _elkjs.ELKInputEdge( + id=link.uuid, sources=[src_obj.uuid], targets=[tgt_obj.uuid] + ) + self.data.edges.append(edge) + self.edges[link.uuid] = edge + return edge + + def _make_box( + self, + obj: t.Any, + **kwargs: t.Any, + ) -> _elkjs.ELKInputChild: + box = makers.make_box( + obj, + **kwargs, + ) + self.boxes[obj.uuid] = box + return box + + def make_port_and_owner( + self, port_obj: m.ModelElement + ) -> _elkjs.ELKInputChild: + owner_obj = port_obj.owner + if not (box := self.boxes.get(owner_obj.uuid)): + box = self._make_box( + owner_obj, + layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS, + ) + self.data.children.append(box) + if port := self.ports.get(port_obj.uuid): + return box + port = makers.make_port(port_obj.uuid) + # port.labels = makers.make_label(port_obj.name) + box.ports.append(port) + self.ports[port_obj.uuid] = port + return box + + +def collector( + diagram: context.ContextDiagram, params: dict[str, t.Any] +) -> _elkjs.ELKInputData: + return CableTreeCollector(diagram, params)() diff --git a/capellambse_context_diagrams/context.py b/capellambse_context_diagrams/context.py index 9e1055a..a178571 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 ( + cable_tree, dataflow_view, exchanges, get_elkdata, @@ -181,6 +182,22 @@ def __get__( # type: ignore return self._get(obj, DataFlowViewDiagram) +class CableTreeAccessor(ContextAccessor): + """Provides access to the cable tree diagrams.""" + + def __get__( # type: ignore + self, + obj: m.T | None, + objtype: type | None = None, + ) -> m.Accessor | ContextDiagram: + """Make a CableTreeView for the given model object.""" + del objtype + if obj is None: # pragma: no cover + return self + assert isinstance(obj, m.ModelElement) + return self._get(obj, CableTreeViewDiagram) + + class ContextDiagram(m.AbstractDiagram): """An automatically generated context diagram. @@ -694,6 +711,27 @@ def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram: return super()._create_diagram(params) +class CableTreeViewDiagram(ContextDiagram): + """An automatically generated CableTreeView.""" + + @property + def uuid(self) -> str: # type: ignore + """Returns the UUID of the diagram.""" + return f"{self.target.uuid}_cable_tree" + + @property + def name(self) -> str: # type: ignore + return f"Cable Tree View of {self.target.name}" + + def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram: + data = cable_tree.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: