Skip to content

Commit

Permalink
feat(context-diagram): Add support for PhysicalPorts
Browse files Browse the repository at this point in the history
  • Loading branch information
huyenngn committed Nov 19, 2024
1 parent ec09614 commit f6341e5
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 13 deletions.
14 changes: 14 additions & 0 deletions capellambse_context_diagrams/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def init() -> None:
"""Initialize the extension."""
register_classes()
register_interface_context()
register_physical_port_context()
register_tree_view()
register_realization_view()
register_data_flow_view()
Expand Down Expand Up @@ -248,6 +249,18 @@ def register_functional_context() -> None:
)


def register_physical_port_context() -> None:
"""Add the `context_diagram` attribute to `PhysicalPort`s."""
m.set_accessor(
cs.PhysicalPort,
ATTR_NAME,
context.PhysicalPortContextAccessor(
DiagramType.PAB.value,
{},
),
)


def register_tree_view() -> None:
"""Add the ``tree_view`` attribute to ``Class``es."""
m.set_accessor(
Expand Down Expand Up @@ -324,6 +337,7 @@ def register_custom_diagram() -> None:
(la.LogicalFunction, DiagramType.LAB),
(pa.PhysicalFunction, DiagramType.PAB),
(fa.ComponentExchange, DiagramType.SAB),
(cs.PhysicalPort, DiagramType.PAB),
]
for class_, dgcls in supported_classes:
m.set_accessor(
Expand Down
33 changes: 28 additions & 5 deletions capellambse_context_diagrams/collectors/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ def _is_edge(obj: m.ModelElement) -> bool:
return False


def _is_port(obj: m.ModelElement) -> bool:
if obj.xtype.endswith("Port"):
return True
return False


class CustomCollector:
"""Collect the context for a custom diagram."""

Expand All @@ -30,9 +36,12 @@ def __init__(
) -> None:
self.diagram = diagram
self.target: m.ModelElement = self.diagram.target
self.boxable_target = (
self.target.source.owner if _is_edge(self.target) else self.target
)
if _is_port(self.target):
self.boxable_target = self.target.owner
elif _is_edge(self.target):
self.boxable_target = self.target.source.owner
else:
self.boxable_target = self.target
self.data = makers.make_diagram(diagram)
self.params = params
self.instructions = self.diagram._collect
Expand All @@ -54,7 +63,10 @@ def __init__(
self.min_heights: dict[str, dict[str, float]] = {}

def __call__(self) -> _elkjs.ELKInputData:
self._make_target(self.target)
if _is_port(self.target):
self._make_port_and_owner(self.target)
else:
self._make_target(self.target)
if target_edge := self.edges.get(self.target.uuid):
target_edge.layoutOptions = copy.deepcopy(
_elkjs.EDGE_STRAIGHTENING_LAYOUT_OPTIONS
Expand Down Expand Up @@ -116,7 +128,7 @@ def _perform_instructions(
self.repeat_depth = max_depth
if get_targets := instructions.get("get"):
self._perform_get_or_include(obj, get_targets, False)
elif include_targets := instructions.get("include"):
if include_targets := instructions.get("include"):
self._perform_get_or_include(obj, include_targets, True)
if not get_targets and not include_targets:
if self.repeat_depth != 0:
Expand Down Expand Up @@ -310,6 +322,17 @@ def _need_switch(
self.directions[tgt_uuid] = not src_dir
if self.directions[src_uuid]:
return True
elif self.diagram._unify_edge_direction == "TREE":
src_dir = self.directions.get(src_uuid)
tgt_dir = self.directions.get(tgt_uuid)
if (src_dir is None) and (tgt_dir is None):
self.directions[src_uuid] = True
self.directions[tgt_uuid] = True
elif src_dir is None:
self.directions[src_uuid] = True
return True
elif tgt_dir is None:
self.directions[tgt_uuid] = True
return False

def _make_port_and_owner(
Expand Down
57 changes: 50 additions & 7 deletions capellambse_context_diagrams/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import logging
import typing as t

import yaml
from capellambse import diagram as cdiagram
from capellambse import helpers
from capellambse import model as m
Expand Down Expand Up @@ -123,6 +124,20 @@ def __get__( # type: ignore
return self._get(obj, FunctionalContextDiagram)


class PhysicalPortContextAccessor(ContextAccessor):
def __get__( # type: ignore
self,
obj: m.T | None,
objtype: type | None = None,
) -> m.Accessor | ContextDiagram:
"""Make a ContextDiagram 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, PhysicalPortContextDiagram)


class ClassTreeAccessor(ContextAccessor):
"""Provides access to the tree view diagrams."""

Expand Down Expand Up @@ -923,14 +938,42 @@ def __init__(
)
self.collector = custom.collector

@property
def uuid(self) -> str: # type: ignore
"""Returns the UUID of the diagram."""
return f"{self.target.uuid}_custom_diagram"

@property
def name(self) -> str: # type: ignore
return f"Custom Diagram of {self.target.name}"
class PhysicalPortContextDiagram(ContextDiagram):
"""An automatically generated Context Diagram exclusively for
PhysicalPorts.
"""

def __init__(
self,
class_: str,
obj: m.ModelElement,
*,
render_styles: dict[str, styling.Styler] | None = None,
default_render_parameters: dict[str, t.Any],
) -> None:
COLLECT_YAML = """
repeat: -1
include:
name: links
get:
- name: source
- name: target
"""
default_render_parameters = {
"collect": yaml.safe_load(COLLECT_YAML),
"display_parent_relation": True,
"unify_edge_direction": "TREE",
"display_port_labels": True,
"port_label_position": _elkjs.PORT_LABEL_POSITION.OUTSIDE.name,
} | default_render_parameters
super().__init__(
class_,
obj,
render_styles=render_styles,
default_render_parameters=default_render_parameters,
)
self.collector = custom.collector


def try_to_layout(data: _elkjs.ELKInputData) -> _elkjs.ELKOutputData:
Expand Down
7 changes: 6 additions & 1 deletion tests/test_context_diagrams.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
TEST_ENTITY_UUID = "e37510b9-3166-4f80-a919-dfaac9b696c7"
TEST_SYS_FNC_UUID = "a5642060-c9cc-4d49-af09-defaa3024bae"
TEST_DERIVATION_UUID = "4ec45aec-0d6a-411a-80ee-ebd3c1a53d2c"
TEST_PHYSICAL_PORT_UUID = "c403d4f4-9633-42a2-a5d6-9e1df2655146"


@pytest.mark.parametrize(
Expand Down Expand Up @@ -53,13 +54,17 @@
"c78b5d7c-be0c-4ed4-9d12-d447cb39304e",
id="PhysicalBehaviorComponent",
),
pytest.param(
TEST_PHYSICAL_PORT_UUID,
id="PhysicalPort",
),
],
)
def test_context_diagrams(model: capellambse.MelodyModel, uuid: str) -> None:
obj = model.by_uuid(uuid)

diag = obj.context_diagram
diag.render(None, display_parent_relation=True)
diag.render("svgdiagram", display_parent_relation=True).save(pretty=True)
diag.render(None, display_parent_relation=False)

assert diag.nodes
Expand Down

0 comments on commit f6341e5

Please sign in to comment.